1
0
mirror of https://github.com/trezor/trezor-firmware.git synced 2024-11-21 23:18:13 +00:00

core: add slip39 keyboard

This commit is contained in:
Tomas Susanka 2019-06-23 12:18:24 +02:00
parent 132519123e
commit 49d6a35249
11 changed files with 1841 additions and 1 deletions

View File

@ -88,6 +88,7 @@ SOURCE_MOD += [
'vendor/trezor-crypto/sha2.c',
'vendor/trezor-crypto/sha3.c',
'vendor/trezor-crypto/shamir.c',
'vendor/trezor-crypto/slip39.c',
]
# libsecp256k1-zkp

View File

@ -86,6 +86,7 @@ SOURCE_MOD += [
'vendor/trezor-crypto/sha2.c',
'vendor/trezor-crypto/sha3.c',
'vendor/trezor-crypto/shamir.c',
'vendor/trezor-crypto/slip39.c',
]
# libsecp256k1-zkp

View File

@ -0,0 +1,122 @@
/*
* This file is part of the TREZOR project, https://trezor.io/
*
* Copyright (c) SatoshiLabs
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program 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 General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "py/obj.h"
#include "py/runtime.h"
#include "slip39.h"
/// package: trezorcrypto.slip39
/// def compute_mask(prefix: int) -> int:
/// """
/// Calculates which buttons still can be pressed after some already were.
/// Returns a 9-bit bitmask, where each bit specifies which buttons
/// can be further pressed (there are still words in this combination).
/// LSB denotes first button.
///
/// Example: 110000110 - second, third, eighth and ninth button still can be
/// pressed.
/// """
STATIC mp_obj_t mod_trezorcrypto_slip39_compute_mask(mp_obj_t _prefix) {
uint16_t prefix = mp_obj_get_int(_prefix);
if (prefix < 1 || prefix > 9999) {
mp_raise_ValueError(
"Invalid button prefix (range between 1 and 9999 is allowed)");
}
return mp_obj_new_int_from_uint(compute_mask(prefix));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_slip39_compute_mask_obj,
mod_trezorcrypto_slip39_compute_mask);
/// def button_sequence_to_word(prefix: int) -> str:
/// """
/// Finds the first word that fits the given button prefix.
/// """
STATIC mp_obj_t
mod_trezorcrypto_slip39_button_sequence_to_word(mp_obj_t _prefix) {
uint16_t prefix = mp_obj_get_int(_prefix);
if (prefix < 1 || prefix > 9999) {
mp_raise_ValueError(
"Invalid button prefix (range between 1 and 9999 is allowed)");
}
const char *word = button_sequence_to_word(prefix);
return mp_obj_new_str_copy(&mp_type_str, (const uint8_t *)word, strlen(word));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(
mod_trezorcrypto_slip39_button_sequence_to_word_obj,
mod_trezorcrypto_slip39_button_sequence_to_word);
/// def word_index(word: str) -> int:
/// """
/// Finds index of given word.
/// Raises ValueError if not found.
/// """
STATIC mp_obj_t mod_trezorcrypto_slip39_word_index(mp_obj_t _word) {
mp_buffer_info_t word;
mp_get_buffer_raise(_word, &word, MP_BUFFER_READ);
uint16_t result = 0;
if (word_index(&result, word.buf, word.len) == false) {
mp_raise_ValueError("Invalid mnemonic word");
}
return mp_obj_new_int_from_uint(result);
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_slip39_word_index_obj,
mod_trezorcrypto_slip39_word_index);
/// def get_word(index: int) -> str:
/// """
/// Returns word on position 'index'.
/// """
STATIC mp_obj_t mod_trezorcrypto_slip39_get_word(mp_obj_t _index) {
uint16_t index = mp_obj_get_int(_index);
if (index > 1023) {
mp_raise_ValueError(
"Invalid wordlist index (range between 0 and 1024 is allowed)");
}
const char *word = get_word(index);
return mp_obj_new_str_copy(&mp_type_str, (const uint8_t *)word, strlen(word));
}
STATIC MP_DEFINE_CONST_FUN_OBJ_1(mod_trezorcrypto_slip39_get_word_obj,
mod_trezorcrypto_slip39_get_word);
STATIC const mp_rom_map_elem_t mod_trezorcrypto_slip39_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_slip39)},
{MP_ROM_QSTR(MP_QSTR_compute_mask),
MP_ROM_PTR(&mod_trezorcrypto_slip39_compute_mask_obj)},
{MP_ROM_QSTR(MP_QSTR_button_sequence_to_word),
MP_ROM_PTR(&mod_trezorcrypto_slip39_button_sequence_to_word_obj)},
{MP_ROM_QSTR(MP_QSTR_word_index),
MP_ROM_PTR(&mod_trezorcrypto_slip39_word_index_obj)},
{MP_ROM_QSTR(MP_QSTR_get_word),
MP_ROM_PTR(&mod_trezorcrypto_slip39_get_word_obj)},
};
STATIC MP_DEFINE_CONST_DICT(mod_trezorcrypto_slip39_globals,
mod_trezorcrypto_slip39_globals_table);
STATIC const mp_obj_module_t mod_trezorcrypto_slip39_module = {
.base = {&mp_type_module},
.globals = (mp_obj_dict_t *)&mod_trezorcrypto_slip39_globals,
};

View File

@ -51,6 +51,7 @@
#include "modtrezorcrypto-sha3-512.h"
#include "modtrezorcrypto-sha512.h"
#include "modtrezorcrypto-shamir.h"
#include "modtrezorcrypto-slip39.h"
STATIC const mp_rom_map_elem_t mp_module_trezorcrypto_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR___name__), MP_ROM_QSTR(MP_QSTR_trezorcrypto)},
@ -91,6 +92,7 @@ STATIC const mp_rom_map_elem_t mp_module_trezorcrypto_globals_table[] = {
{MP_ROM_QSTR(MP_QSTR_sha3_512),
MP_ROM_PTR(&mod_trezorcrypto_Sha3_512_type)},
{MP_ROM_QSTR(MP_QSTR_shamir), MP_ROM_PTR(&mod_trezorcrypto_shamir_module)},
{MP_ROM_QSTR(MP_QSTR_slip39), MP_ROM_PTR(&mod_trezorcrypto_slip39_module)},
};
STATIC MP_DEFINE_CONST_DICT(mp_module_trezorcrypto_globals,
mp_module_trezorcrypto_globals_table);

View File

@ -90,7 +90,7 @@ class Prompt(ui.Control):
self.repaint = False
class MnemonicKeyboard(ui.Layout):
class Bip39Keyboard(ui.Layout):
def __init__(self, prompt):
self.prompt = Prompt(prompt)

View File

@ -0,0 +1,205 @@
from trezor import io, loop, res, ui
from trezor.crypto import slip39
from trezor.ui import display
from trezor.ui.button import Button, ButtonClear, ButtonMono, ButtonMonoConfirm
def check_mask(mask: int, index: int) -> bool:
return bool((1 << (index - 1)) & mask)
# TODO: ask UX if we want to finish sooner than 4 words
# example: 'hairy'
def _is_final(content: str) -> bool:
return len(content) > 3
class KeyButton(Button):
def __init__(self, area, content, keyboard, index):
self.keyboard = keyboard
self.index = index
super().__init__(area, content)
def on_click(self):
self.keyboard.on_key_click(self)
class InputButton(Button):
def __init__(self, area, content, word):
super().__init__(area, content)
self.word = word
self.pending_button = None
self.pending_index = None
self.icon = None # rendered icon
self.disable()
def edit(self, content, word, pending_button, pending_index):
self.word = word
self.content = content
self.pending_button = pending_button
self.pending_index = pending_index
self.repaint = True
if word:
self.enable()
self.normal_style = ButtonMonoConfirm.normal
self.active_style = ButtonMonoConfirm.active
self.icon = ui.ICON_CONFIRM
else: # disabled button
self.disabled_style = ButtonMono.normal
self.disable()
self.icon = None
def render_content(self, s, ax, ay, aw, ah):
text_style = s.text_style
fg_color = s.fg_color
bg_color = s.bg_color
tx = ax + 24 # x-offset of the content
ty = ay + ah // 2 + 8 # y-offset of the content
if not _is_final(self.content):
to_display = len(self.content) * "*"
if self.pending_button:
to_display = (
to_display[:-1] + self.pending_button.content[self.pending_index]
)
else:
to_display = self.word
display.text(tx, ty, to_display, text_style, fg_color, bg_color)
if self.pending_button and not _is_final(self.content):
width = display.text_width(to_display, text_style)
pw = display.text_width(self.content[-1:], text_style)
px = tx + width - pw
display.bar(px, ty + 2, pw + 1, 3, fg_color)
if self.icon:
ix = ax + aw - 16 * 2
iy = ty - 16
display.icon(ix, iy, res.load(self.icon), fg_color, bg_color)
class Prompt(ui.Control):
def __init__(self, prompt):
self.prompt = prompt
self.repaint = True
def on_render(self):
if self.repaint:
display.bar(0, 8, ui.WIDTH, 60, ui.BG)
display.text(20, 40, self.prompt, ui.BOLD, ui.GREY, ui.BG)
self.repaint = False
class Slip39Keyboard(ui.Layout):
def __init__(self, prompt):
self.prompt = Prompt(prompt)
icon_back = res.load(ui.ICON_BACK)
self.back = Button(ui.grid(0, n_x=4, n_y=4), icon_back, ButtonClear)
self.back.on_click = self.on_back_click
self.input = InputButton(ui.grid(1, n_x=4, n_y=4, cells_x=3), "", "")
self.input.on_click = self.on_input_click
self.keys = [
KeyButton(ui.grid(i + 3, n_y=4), k, self, i + 1)
for i, k in enumerate(
("ab", "cd", "ef", "ghij", "klm", "nopq", "rs", "tuv", "wxyz")
)
]
self.pending_button = None
self.pending_index = 0
self.button_sequence = ""
def dispatch(self, event: int, x: int, y: int):
for btn in self.keys:
btn.dispatch(event, x, y)
if self.input.content:
self.input.dispatch(event, x, y)
self.back.dispatch(event, x, y)
else:
self.prompt.dispatch(event, x, y)
def on_back_click(self):
# Backspace was clicked, let's delete the last character of input.
self.button_sequence = self.button_sequence[:-1]
self.edit()
def on_input_click(self):
# Input button was clicked. If the content matches the suggested word,
# let's confirm it, otherwise just auto-complete.
result = self.input.word
if _is_final(self.input.content):
self.button_sequence = ""
self.edit()
self.on_confirm(result)
def on_key_click(self, btn: KeyButton):
# Key button was clicked. If this button is pending, let's cycle the
# pending character in input. If not, let's just append the first
# character.
if self.pending_button is btn:
index = (self.pending_index + 1) % len(btn.content)
else:
index = 0
self.button_sequence += str(btn.index)
self.edit(btn, index)
def on_timeout(self):
# Timeout occurred. Let's redraw to draw asterisks.
self.edit()
def on_confirm(self, word):
# Word was confirmed by the user.
raise ui.Result(word)
def edit(self, button: KeyButton = None, index: int = 0):
self.pending_button = button
self.pending_index = index
# find the completions
mask = 0
word = ""
if _is_final(self.button_sequence):
word = slip39.button_sequence_to_word(self.button_sequence)
else:
mask = slip39.compute_mask(self.button_sequence)
# modify the input state
self.input.edit(
self.button_sequence, word, self.pending_button, self.pending_index
)
# enable or disable key buttons
for btn in self.keys:
if (not _is_final(self.button_sequence) and btn is button) or check_mask(
mask, btn.index
):
btn.enable()
else:
btn.disable()
# invalidate the prompt if we display it next frame
if not self.input.content:
self.prompt.repaint = True
async def handle_input(self):
touch = loop.wait(io.TOUCH)
timeout = loop.sleep(1000 * 1000 * 1)
spawn_touch = loop.spawn(touch)
spawn_timeout = loop.spawn(touch, timeout)
while True:
if self.pending_button is not None:
spawn = spawn_timeout
else:
spawn = spawn_touch
result = await spawn
if touch in spawn.finished:
event, x, y = result
self.dispatch(event, x, y)
else:
self.on_timeout()

View File

@ -66,6 +66,7 @@ SRCS += memzero.c
SRCS += shamir.c
SRCS += hmac_drbg.c
SRCS += rfc6979.c
SRCS += slip39.c
OBJS = $(SRCS:.c=.o)

136
crypto/slip39.c Normal file
View File

@ -0,0 +1,136 @@
/**
* This file is part of the TREZOR project, https://trezor.io/
*
* Copyright (c) SatoshiLabs
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
* OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#include "slip39.h"
#include <stdio.h>
#include <string.h>
#include "slip39_wordlist.h"
/**
* Returns word on position `index`.
*/
const char* get_word(uint16_t index) { return wordlist[index]; }
/**
* Finds index of given word, if found.
* Returns true on success and stores result in `index`.
*/
bool word_index(uint16_t* index, const char* word, uint8_t word_length) {
uint16_t lo = 0;
uint16_t hi = WORDS_COUNT;
uint16_t mid = 0;
while ((hi - lo) > 1) {
mid = (hi + lo) / 2;
if (strncmp(wordlist[mid], word, word_length) > 0) {
hi = mid;
} else {
lo = mid;
}
}
if (strncmp(wordlist[lo], word, word_length) != 0) {
return false;
}
*index = lo;
return true;
}
/**
* Calculates which buttons still can be pressed after some already were.
* Returns a 9-bit bitmask, where each bit specifies which buttons
* can be further pressed (there are still words in this combination).
* LSB denotes first button.
*
* Example: 110000110 - second, third, eighth and ninth button still can be
* pressed.
*/
uint16_t compute_mask(uint16_t prefix) { return find(prefix, false); }
/**
* Converts sequence to word index.
*/
const char* button_sequence_to_word(uint16_t prefix) {
return wordlist[find(prefix, true)];
}
/**
* Computes mask if find_index is false.
* Finds the first word index that suits the prefix otherwise.
*/
uint16_t find(uint16_t prefix, bool find_index) {
uint16_t min = prefix;
uint16_t max = 0;
uint16_t for_max = 0;
uint8_t multiplier = 0;
uint8_t divider = 0;
uint16_t bitmap = 0;
uint8_t digit = 0;
uint16_t i = 0;
max = prefix + 1;
while (min < 1000) {
min = min * 10;
max = max * 10;
multiplier++;
}
// Four char prefix -> the mask is zero
if (!multiplier && !find_index) {
return 0;
}
for_max = min - (min % 1000) + 1000;
// We can't use binary search because the numbers are not sorted.
// They are sorted using the words' alphabet (so we can use the index).
// Example: axle (1953), beam (1315)
// The first digit is sorted so we can loop only upto `for_max`.
while (words_button_seq[i] < for_max) {
if (words_button_seq[i] >= min && words_button_seq[i] < max) {
if (find_index) {
return i;
}
switch (multiplier) {
case 1:
divider = 1;
break;
case 2:
divider = 10;
break;
case 3:
divider = 100;
break;
default:
divider = 1;
break;
}
digit = (words_button_seq[i] / divider) % 10;
bitmap |= 1 << (digit - 1);
}
i++;
}
return bitmap;
}

36
crypto/slip39.h Normal file
View File

@ -0,0 +1,36 @@
/**
* This file is part of the TREZOR project, https://trezor.io/
*
* Copyright (c) SatoshiLabs
*
* Permission is hereby granted, free of charge, to any person obtaining
* a copy of this software and associated documentation files (the "Software"),
* to deal in the Software without restriction, including without limitation
* the rights to use, copy, modify, merge, publish, distribute, sublicense,
* and/or sell copies of the Software, and to permit persons to whom the
* Software is furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included
* in all copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
* OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
* THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
* OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
* ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
* OTHER DEALINGS IN THE SOFTWARE.
*/
#include <stdbool.h>
#include <stdint.h>
const char* get_word(uint16_t index);
bool word_index(uint16_t* index, const char* word, uint8_t word_length);
uint16_t compute_mask(uint16_t prefix);
const char* button_sequence_to_word(uint16_t prefix);
uint16_t find(uint16_t prefix, bool find_index);

1238
crypto/slip39_wordlist.h Normal file

File diff suppressed because it is too large Load Diff

View File

@ -67,6 +67,7 @@
#include "sha2.h"
#include "sha3.h"
#include "shamir.h"
#include "slip39.h"
#if VALGRIND
/*
@ -5096,6 +5097,96 @@ START_TEST(test_mnemonic_to_entropy) {
}
END_TEST
START_TEST(test_slip39_get_word) {
static const struct {
const int index;
const char *expected_word;
} vectors[] = {{573, "member"},
{0, "academic"},
{1023, "zero"},
{245, "drove"},
{781, "satoshi"}};
for (size_t i = 0; i < (sizeof(vectors) / sizeof(*vectors)); i++) {
const char *a = get_word(vectors[i].index);
ck_assert_str_eq(a, vectors[i].expected_word);
}
}
END_TEST
START_TEST(test_slip39_word_index) {
uint16_t index;
static const struct {
const char *word;
bool expected_result;
uint16_t expected_index;
} vectors[] = {{"academic", true, 0},
{"zero", true, 1023},
{"drove", true, 245},
{"satoshi", true, 781},
{"member", true, 573},
// 9999 value is never checked since the word is not in list
{"fakeword", false, 9999}};
for (size_t i = 0; i < (sizeof(vectors) / sizeof(*vectors)); i++) {
bool result = word_index(&index, vectors[i].word, sizeof(vectors[i].word));
ck_assert_int_eq(result, vectors[i].expected_result);
if (result) {
ck_assert_int_eq(index, vectors[i].expected_index);
}
}
}
END_TEST
START_TEST(test_slip39_compute_mask) {
static const struct {
const uint16_t prefix;
const uint16_t expected_mask;
} vectors[] = {{
12,
0xFD // 011111101
},
{
21,
0xF8 // 011111000
},
{
75,
0xAD // 010101101
},
{
4,
0x1F7 // 111110111
},
{
738,
0x6D // 001101101
},
{
9,
0x6D // 001101101
}};
for (size_t i = 0; i < (sizeof(vectors) / sizeof(*vectors)); i++) {
uint16_t mask = compute_mask(vectors[i].prefix);
ck_assert_int_eq(mask, vectors[i].expected_mask);
}
}
END_TEST
START_TEST(test_slip39_sequence_to_word) {
static const struct {
const uint16_t prefix;
const char *expected_word;
} vectors[] = {{7945, "swimming"},
{646, "photo"},
{5, "kernel"},
{34, "either"},
{62, "ocean"}};
for (size_t i = 0; i < (sizeof(vectors) / sizeof(*vectors)); i++) {
const char *word = button_sequence_to_word(vectors[i].prefix);
ck_assert_str_eq(word, vectors[i].expected_word);
}
}
END_TEST
START_TEST(test_shamir) {
#define SHAMIR_MAX_COUNT 16
static const struct {
@ -8676,6 +8767,13 @@ Suite *test_suite(void) {
tcase_add_test(tc, test_mnemonic_to_entropy);
suite_add_tcase(s, tc);
tc = tcase_create("slip39");
tcase_add_test(tc, test_slip39_get_word);
tcase_add_test(tc, test_slip39_word_index);
tcase_add_test(tc, test_slip39_compute_mask);
tcase_add_test(tc, test_slip39_sequence_to_word);
suite_add_tcase(s, tc);
tc = tcase_create("shamir");
tcase_add_test(tc, test_shamir);
suite_add_tcase(s, tc);