mirror of
https://github.com/trezor/trezor-firmware.git
synced 2025-01-18 03:10:58 +00:00
feat(core): Account for Optiga throttling delay in PIN countdown.
This commit is contained in:
parent
d82d5a1fe5
commit
9420b38a35
1
core/.changelog.d/4000.added
Normal file
1
core/.changelog.d/4000.added
Normal file
@ -0,0 +1 @@
|
||||
Include Optiga throttling delay in PIN countdown.
|
@ -423,6 +423,7 @@ SOURCE_UNIX = [
|
||||
'embed/trezorhal/unix/flash.c',
|
||||
'embed/trezorhal/unix/random_delays.c',
|
||||
'embed/trezorhal/unix/rng.c',
|
||||
'embed/trezorhal/unix/time_estimate.c',
|
||||
'embed/trezorhal/unix/usb.c',
|
||||
'embed/unix/main_main.c',
|
||||
'embed/unix/main.c',
|
||||
|
@ -24,7 +24,7 @@
|
||||
#include <stddef.h>
|
||||
#include <stdint.h>
|
||||
#include "optiga_common.h"
|
||||
#include "secbool.h"
|
||||
#include "storage.h"
|
||||
|
||||
#define OPTIGA_DEVICE_CERT_INDEX 1
|
||||
#define OPTIGA_DEVICE_ECC_KEY_INDEX 0
|
||||
@ -46,14 +46,6 @@ typedef enum _optiga_sign_result {
|
||||
// Size of secrets used in PIN processing, e.g. salted PIN, master secret etc.
|
||||
#define OPTIGA_PIN_SECRET_SIZE 32
|
||||
|
||||
// The number of milliseconds it takes to execute optiga_pin_set().
|
||||
#define OPTIGA_PIN_SET_MS 1300
|
||||
|
||||
// The number of milliseconds it takes to execute optiga_pin_verify().
|
||||
#define OPTIGA_PIN_VERIFY_MS 900
|
||||
|
||||
typedef secbool (*OPTIGA_UI_PROGRESS)(uint32_t elapsed_ms);
|
||||
|
||||
optiga_sign_result __wur optiga_sign(uint8_t index, const uint8_t *digest,
|
||||
size_t digest_size, uint8_t *signature,
|
||||
size_t max_sig_size, size_t *sig_size);
|
||||
@ -67,15 +59,17 @@ bool __wur optiga_read_sec(uint8_t *sec);
|
||||
|
||||
bool __wur optiga_random_buffer(uint8_t *dest, size_t size);
|
||||
|
||||
bool __wur optiga_pin_set(OPTIGA_UI_PROGRESS ui_progress,
|
||||
bool __wur optiga_pin_set(optiga_ui_progress_t ui_progress,
|
||||
uint8_t stretched_pin[OPTIGA_PIN_SECRET_SIZE]);
|
||||
|
||||
uint32_t optiga_estimate_time_ms(storage_pin_op_t op);
|
||||
|
||||
optiga_pin_result __wur
|
||||
optiga_pin_verify(OPTIGA_UI_PROGRESS ui_progress,
|
||||
optiga_pin_verify(optiga_ui_progress_t ui_progress,
|
||||
uint8_t stretched_pin[OPTIGA_PIN_SECRET_SIZE]);
|
||||
|
||||
optiga_pin_result __wur
|
||||
optiga_pin_verify_v4(OPTIGA_UI_PROGRESS ui_progress,
|
||||
optiga_pin_verify_v4(optiga_ui_progress_t ui_progress,
|
||||
const uint8_t pin_secret[OPTIGA_PIN_SECRET_SIZE],
|
||||
uint8_t out_secret[OPTIGA_PIN_SECRET_SIZE]);
|
||||
|
||||
|
@ -23,6 +23,7 @@
|
||||
#include "hmac.h"
|
||||
#include "memzero.h"
|
||||
#include "optiga_commands.h"
|
||||
#include "optiga_transport.h"
|
||||
#include "rand.h"
|
||||
#include "storage.h"
|
||||
|
||||
@ -59,6 +60,9 @@
|
||||
// The number of times that PIN stretching is repeated.
|
||||
#define PIN_STRETCH_ITERATIONS 2
|
||||
|
||||
// The throttling delay when the security event counter is at its maximum.
|
||||
#define OPTIGA_T_MAX_MS 5000
|
||||
|
||||
// Value of the PIN counter when it is reset.
|
||||
static const uint8_t COUNTER_RESET[] = {0, 0, 0, 0, 0, 0, 0, PIN_MAX_TRIES};
|
||||
|
||||
@ -172,6 +176,37 @@ bool optiga_read_sec(uint8_t *sec) {
|
||||
return ret == OPTIGA_SUCCESS && size == sizeof(uint8_t);
|
||||
}
|
||||
|
||||
uint32_t optiga_estimate_time_ms(storage_pin_op_t op) {
|
||||
uint8_t sec = 0;
|
||||
if (!optiga_read_sec(&sec)) {
|
||||
return UINT32_MAX;
|
||||
}
|
||||
|
||||
// Heuristic: The SEC will increase by about 4 during the operation up to a
|
||||
// maximum of 255.
|
||||
sec = (sec < 255 - 4) ? sec + 4 : 255;
|
||||
|
||||
// If the SEC is above 127, then Optiga introduces a throttling delay before
|
||||
// the execution of each protected command. The delay grows propotionally to
|
||||
// the SEC value up to a maximum delay of OPTIGA_T_MAX_MS.
|
||||
uint32_t throttling_delay =
|
||||
sec > 127 ? (sec - 127) * OPTIGA_T_MAX_MS / 128 : 0;
|
||||
|
||||
// To estimate the overall time of the PIN operation we multiply the
|
||||
// throttling delay by the number of protected Optiga commands and add the
|
||||
// time required to execute all Optiga commands without throttling delays.
|
||||
switch (op) {
|
||||
case STORAGE_PIN_OP_SET:
|
||||
return throttling_delay * 6 + 1300;
|
||||
case STORAGE_PIN_OP_VERIFY:
|
||||
return throttling_delay * 7 + 1000;
|
||||
case STORAGE_PIN_OP_CHANGE:
|
||||
return throttling_delay * 13 + 2300;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
bool optiga_random_buffer(uint8_t *dest, size_t size) {
|
||||
while (size > OPTIGA_RANDOM_MAX_SIZE) {
|
||||
if (optiga_get_random(dest, OPTIGA_RANDOM_MAX_SIZE) != OPTIGA_SUCCESS) {
|
||||
@ -371,7 +406,7 @@ static bool optiga_pin_init_stretch(void) {
|
||||
}
|
||||
|
||||
static bool optiga_pin_stretch_common(
|
||||
OPTIGA_UI_PROGRESS ui_progress, HMAC_SHA256_CTX *ctx,
|
||||
optiga_ui_progress_t ui_progress, HMAC_SHA256_CTX *ctx,
|
||||
const uint8_t input[OPTIGA_PIN_SECRET_SIZE], bool version4) {
|
||||
// Implements the functionality that is common to
|
||||
// optiga_pin_stretch_cmac_ecdh() and the legacy function
|
||||
@ -417,7 +452,7 @@ static bool optiga_pin_stretch_common(
|
||||
goto end;
|
||||
}
|
||||
|
||||
ui_progress(250);
|
||||
ui_progress();
|
||||
|
||||
hmac_sha256_Update(ctx, buffer, size);
|
||||
|
||||
@ -428,7 +463,7 @@ end:
|
||||
}
|
||||
|
||||
static bool optiga_pin_stretch_secret_v4(
|
||||
OPTIGA_UI_PROGRESS ui_progress, uint8_t secret[OPTIGA_PIN_SECRET_SIZE]) {
|
||||
optiga_ui_progress_t ui_progress, uint8_t secret[OPTIGA_PIN_SECRET_SIZE]) {
|
||||
// Legacy PIN verification method used in storage versions 3 and 4.
|
||||
|
||||
// This step hardens the PIN verification process in case an attacker is able
|
||||
@ -459,7 +494,7 @@ static bool optiga_pin_stretch_secret_v4(
|
||||
}
|
||||
|
||||
static bool optiga_pin_stretch_cmac_ecdh(
|
||||
OPTIGA_UI_PROGRESS ui_progress,
|
||||
optiga_ui_progress_t ui_progress,
|
||||
uint8_t stretched_pin[OPTIGA_PIN_SECRET_SIZE]) {
|
||||
// This step hardens the PIN verification process in case an attacker is able
|
||||
// to extract the secret value of a data object in Optiga that has a
|
||||
@ -503,15 +538,17 @@ end:
|
||||
return ret;
|
||||
}
|
||||
|
||||
bool optiga_pin_set(OPTIGA_UI_PROGRESS ui_progress,
|
||||
bool optiga_pin_set(optiga_ui_progress_t ui_progress,
|
||||
uint8_t stretched_pin[OPTIGA_PIN_SECRET_SIZE]) {
|
||||
optiga_set_ui_progress(ui_progress);
|
||||
|
||||
bool ret = true;
|
||||
if (!optiga_pin_init_metadata() || !optiga_pin_init_stretch()) {
|
||||
ret = false;
|
||||
goto end;
|
||||
}
|
||||
|
||||
ui_progress(300);
|
||||
ui_progress();
|
||||
|
||||
// Stretch the PIN more with stretching secrets from the Optiga. This step
|
||||
// ensures that if an attacker extracts the value of OID_STRETCHED_PIN or
|
||||
@ -590,7 +627,7 @@ bool optiga_pin_set(OPTIGA_UI_PROGRESS ui_progress,
|
||||
goto end;
|
||||
}
|
||||
|
||||
ui_progress(250);
|
||||
ui_progress();
|
||||
|
||||
// Authorise using OID_STRETCHED_PIN so that we can write to OID_PIN_HMAC and
|
||||
// OID_PIN_HMAC_CTR.
|
||||
@ -614,7 +651,7 @@ bool optiga_pin_set(OPTIGA_UI_PROGRESS ui_progress,
|
||||
goto end;
|
||||
}
|
||||
|
||||
ui_progress(250);
|
||||
ui_progress();
|
||||
|
||||
// Stretch the PIN more with the counter-protected PIN secret. This method
|
||||
// ensures that if the user chooses a high-entropy PIN, then even if the
|
||||
@ -631,15 +668,17 @@ end:
|
||||
memzero(digest, sizeof(digest));
|
||||
optiga_clear_auto_state(OID_PIN_SECRET);
|
||||
optiga_clear_auto_state(OID_STRETCHED_PIN);
|
||||
optiga_set_ui_progress(NULL);
|
||||
return ret;
|
||||
}
|
||||
|
||||
optiga_pin_result optiga_pin_verify_v4(
|
||||
OPTIGA_UI_PROGRESS ui_progress,
|
||||
optiga_ui_progress_t ui_progress,
|
||||
const uint8_t pin_secret[OPTIGA_PIN_SECRET_SIZE],
|
||||
uint8_t out_secret[OPTIGA_PIN_SECRET_SIZE]) {
|
||||
// Legacy PIN verification method used in storage version 3 and 4.
|
||||
|
||||
optiga_set_ui_progress(ui_progress);
|
||||
optiga_pin_result ret = OPTIGA_PIN_SUCCESS;
|
||||
|
||||
// Process the PIN-derived secret using a one-way function before sending it
|
||||
@ -687,7 +726,7 @@ optiga_pin_result optiga_pin_verify_v4(
|
||||
goto end;
|
||||
}
|
||||
|
||||
ui_progress(200);
|
||||
ui_progress();
|
||||
|
||||
// Authorise using OID_PIN_SECRET so that we can write to OID_PIN_COUNTER.
|
||||
if (optiga_set_auto_state(OPTIGA_OID_SESSION_CTX, OID_PIN_SECRET, out_secret,
|
||||
@ -696,7 +735,7 @@ optiga_pin_result optiga_pin_verify_v4(
|
||||
goto end;
|
||||
}
|
||||
|
||||
ui_progress(200);
|
||||
ui_progress();
|
||||
|
||||
// Combine the value of OID_PIN_SECRET with the PIN-derived secret and
|
||||
// stretching secrets from the Optiga.
|
||||
@ -715,6 +754,7 @@ optiga_pin_result optiga_pin_verify_v4(
|
||||
end:
|
||||
memzero(stretched_pin, sizeof(stretched_pin));
|
||||
optiga_clear_auto_state(OID_STRETCHED_PIN);
|
||||
optiga_set_ui_progress(NULL);
|
||||
return ret;
|
||||
}
|
||||
|
||||
@ -756,8 +796,9 @@ end:
|
||||
}
|
||||
|
||||
optiga_pin_result optiga_pin_verify(
|
||||
OPTIGA_UI_PROGRESS ui_progress,
|
||||
optiga_ui_progress_t ui_progress,
|
||||
uint8_t stretched_pin[OPTIGA_PIN_SECRET_SIZE]) {
|
||||
optiga_set_ui_progress(ui_progress);
|
||||
optiga_pin_result ret = OPTIGA_PIN_SUCCESS;
|
||||
|
||||
// Stretch the PIN more with stretching secrets from the Optiga.
|
||||
@ -801,7 +842,7 @@ optiga_pin_result optiga_pin_verify(
|
||||
goto end;
|
||||
}
|
||||
|
||||
ui_progress(200);
|
||||
ui_progress();
|
||||
|
||||
// Reset the counter which limits the use of OID_PIN_HMAC.
|
||||
if (optiga_set_data_object(OID_PIN_HMAC_CTR, false, COUNTER_RESET,
|
||||
@ -837,13 +878,14 @@ optiga_pin_result optiga_pin_verify(
|
||||
goto end;
|
||||
}
|
||||
|
||||
ui_progress(200);
|
||||
ui_progress();
|
||||
|
||||
end:
|
||||
memzero(pin_secret, sizeof(pin_secret));
|
||||
memzero(digest, sizeof(digest));
|
||||
optiga_clear_auto_state(OID_STRETCHED_PIN);
|
||||
optiga_clear_auto_state(OID_PIN_SECRET);
|
||||
optiga_set_ui_progress(NULL);
|
||||
return ret;
|
||||
}
|
||||
|
||||
|
@ -114,6 +114,7 @@ static uint8_t frame_num_out = 0xff;
|
||||
static uint8_t frame_num_in = 0xff;
|
||||
static uint8_t frame_buffer[1 + OPTIGA_DATA_REG_LEN];
|
||||
static size_t frame_size = 0; // Set by optiga_read().
|
||||
static optiga_ui_progress_t ui_progress = NULL;
|
||||
|
||||
// Secure channel constants.
|
||||
#define SEC_CHAN_SCTR_SIZE 1
|
||||
@ -169,6 +170,8 @@ void optiga_transport_set_log_hex(optiga_log_hex_t f) { log_hex = f; }
|
||||
}
|
||||
#endif
|
||||
|
||||
void optiga_set_ui_progress(optiga_ui_progress_t f) { ui_progress = f; }
|
||||
|
||||
static uint16_t calc_crc_byte(uint16_t seed, uint8_t c) {
|
||||
uint16_t h1 = (seed ^ c) & 0xFF;
|
||||
uint16_t h2 = h1 & 0x0F;
|
||||
@ -430,6 +433,10 @@ static optiga_result optiga_read(void) {
|
||||
}
|
||||
not_busy_count += 1;
|
||||
}
|
||||
|
||||
if (ui_progress != NULL) {
|
||||
ui_progress();
|
||||
}
|
||||
} while (hal_ticks_ms() < deadline);
|
||||
|
||||
return OPTIGA_ERR_TIMEOUT;
|
||||
|
@ -20,6 +20,8 @@
|
||||
#ifndef TREZORHAL_OPTIGA_COMMON_H
|
||||
#define TREZORHAL_OPTIGA_COMMON_H
|
||||
|
||||
#include "secbool.h"
|
||||
|
||||
typedef enum _optiga_result {
|
||||
OPTIGA_SUCCESS = 0, // Operation completed successfully.
|
||||
OPTIGA_ERR_I2C_WRITE, // HAL failed on I2C write.
|
||||
@ -34,6 +36,8 @@ typedef enum _optiga_result {
|
||||
OPTIGA_ERR_CMD, // Command error. See error code data object 0xF1C2.
|
||||
} optiga_result;
|
||||
|
||||
typedef secbool (*optiga_ui_progress_t)(void);
|
||||
|
||||
#if !PRODUCTION
|
||||
typedef void (*optiga_log_hex_t)(const char *prefix, const uint8_t *data,
|
||||
size_t data_size);
|
||||
|
@ -44,6 +44,8 @@ optiga_result optiga_resync(void);
|
||||
optiga_result optiga_soft_reset(void);
|
||||
optiga_result optiga_set_data_reg_len(size_t size);
|
||||
|
||||
void optiga_set_ui_progress(optiga_ui_progress_t f);
|
||||
|
||||
#if !PRODUCTION
|
||||
void optiga_transport_set_log_hex(optiga_log_hex_t f);
|
||||
#endif
|
||||
|
28
core/embed/trezorhal/stm32f4/time_estimate.c
Normal file
28
core/embed/trezorhal/stm32f4/time_estimate.c
Normal file
@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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 "time_estimate.h"
|
||||
|
||||
// The number of CPU cycles required to execute one iteration of PBKDF2.
|
||||
#define PIN_PBKDF2_CYCLES_PER_ITER 11100
|
||||
|
||||
uint32_t time_estimate_pbkdf2_ms(uint32_t iterations) {
|
||||
extern uint32_t SystemCoreClock;
|
||||
return PIN_PBKDF2_CYCLES_PER_ITER * iterations / (SystemCoreClock / 1000);
|
||||
}
|
1
core/embed/trezorhal/stm32u5/time_estimate.c
Symbolic link
1
core/embed/trezorhal/stm32u5/time_estimate.c
Symbolic link
@ -0,0 +1 @@
|
||||
../stm32f4/time_estimate.c
|
27
core/embed/trezorhal/time_estimate.h
Normal file
27
core/embed/trezorhal/time_estimate.h
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef TREZORHAL_TIME_ESTIMATE_H
|
||||
#define TREZORHAL_TIME_ESTIMATE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
uint32_t time_estimate_pbkdf2_ms(uint32_t iterations);
|
||||
|
||||
#endif
|
@ -88,30 +88,29 @@ bool optiga_read_sec(uint8_t *sec) {
|
||||
return true;
|
||||
}
|
||||
|
||||
uint32_t optiga_estimate_time_ms(storage_pin_op_t op) { return 0; }
|
||||
|
||||
bool optiga_random_buffer(uint8_t *dest, size_t size) {
|
||||
random_buffer(dest, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool optiga_pin_set(OPTIGA_UI_PROGRESS ui_progress,
|
||||
bool optiga_pin_set(optiga_ui_progress_t ui_progress,
|
||||
uint8_t stretched_pin[OPTIGA_PIN_SECRET_SIZE]) {
|
||||
ui_progress(OPTIGA_PIN_SET_MS);
|
||||
return true;
|
||||
}
|
||||
|
||||
optiga_pin_result optiga_pin_verify_v4(
|
||||
OPTIGA_UI_PROGRESS ui_progress,
|
||||
optiga_ui_progress_t ui_progress,
|
||||
const uint8_t pin_secret[OPTIGA_PIN_SECRET_SIZE],
|
||||
uint8_t out_secret[OPTIGA_PIN_SECRET_SIZE]) {
|
||||
memcpy(out_secret, pin_secret, OPTIGA_PIN_SECRET_SIZE);
|
||||
ui_progress(OPTIGA_PIN_VERIFY_MS);
|
||||
return OPTIGA_PIN_SUCCESS;
|
||||
}
|
||||
|
||||
optiga_pin_result optiga_pin_verify(
|
||||
OPTIGA_UI_PROGRESS ui_progress,
|
||||
optiga_ui_progress_t ui_progress,
|
||||
uint8_t stretched_pin[OPTIGA_PIN_SECRET_SIZE]) {
|
||||
ui_progress(OPTIGA_PIN_VERIFY_MS);
|
||||
return OPTIGA_PIN_SUCCESS;
|
||||
}
|
||||
|
||||
|
25
core/embed/trezorhal/unix/time_estimate.c
Normal file
25
core/embed/trezorhal/unix/time_estimate.c
Normal file
@ -0,0 +1,25 @@
|
||||
/*
|
||||
* 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 "time_estimate.h"
|
||||
|
||||
uint32_t time_estimate_pbkdf2_ms(uint32_t iterations) {
|
||||
(void)iterations;
|
||||
return 500;
|
||||
}
|
@ -52,6 +52,7 @@ def stm32f4_common_files(env, defines, sources, paths):
|
||||
"embed/trezorhal/stm32f4/secret.c",
|
||||
"embed/trezorhal/stm32f4/systick.c",
|
||||
"embed/trezorhal/stm32f4/supervise.c",
|
||||
"embed/trezorhal/stm32f4/time_estimate.c",
|
||||
"embed/trezorhal/stm32f4/random_delays.c",
|
||||
"embed/trezorhal/stm32f4/rng.c",
|
||||
"embed/trezorhal/stm32f4/vectortable.s",
|
||||
|
@ -66,6 +66,7 @@ def stm32u5_common_files(env, defines, sources, paths):
|
||||
"embed/trezorhal/stm32u5/random_delays.c",
|
||||
"embed/trezorhal/stm32u5/rng.c",
|
||||
"embed/trezorhal/stm32u5/tamper.c",
|
||||
"embed/trezorhal/stm32u5/time_estimate.c",
|
||||
"embed/trezorhal/stm32u5/trustzone.c",
|
||||
"embed/trezorhal/stm32u5/vectortable.s",
|
||||
]
|
||||
|
@ -15,6 +15,7 @@ OBJS += oled.o
|
||||
OBJS += random_delays.o
|
||||
OBJS += rng.o
|
||||
OBJS += supervise.o
|
||||
OBJS += time_estimate.o
|
||||
OBJS += usb21_standard.o
|
||||
OBJS += usb_standard.o
|
||||
OBJS += util.o
|
||||
|
@ -26,6 +26,7 @@
|
||||
#include "layout.h"
|
||||
#include "oled.h"
|
||||
#include "rng.h"
|
||||
#include "timer.h"
|
||||
#include "util.h"
|
||||
|
||||
uint8_t HW_ENTROPY_DATA[HW_ENTROPY_LEN];
|
||||
@ -98,6 +99,8 @@ void hal_delay(uint32_t ms) {
|
||||
#endif
|
||||
}
|
||||
|
||||
uint32_t hal_ticks_ms(void) { return timer_ms(); }
|
||||
|
||||
void drbg_init(void) {
|
||||
uint8_t entropy[48] = {0};
|
||||
random_buffer(entropy, sizeof(entropy));
|
||||
|
@ -39,6 +39,7 @@ void show_pin_too_many_screen(void);
|
||||
(((expr) == sectrue) ? (void)0 : __fatal_error(msg, __FILE__, __LINE__))
|
||||
|
||||
void hal_delay(uint32_t ms);
|
||||
uint32_t hal_ticks_ms(void);
|
||||
|
||||
void drbg_init(void);
|
||||
void drbg_reseed(const uint8_t *entropy, size_t len);
|
||||
|
35
legacy/time_estimate.c
Normal file
35
legacy/time_estimate.c
Normal file
@ -0,0 +1,35 @@
|
||||
/*
|
||||
* 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 "time_estimate.h"
|
||||
|
||||
// The number of CPU cycles required to execute one iteration of PBKDF2.
|
||||
#define PIN_PBKDF2_CYCLES_PER_ITER 11100
|
||||
|
||||
// MCU clock 120 MHz
|
||||
#define MCU_CLOCK 120000000
|
||||
|
||||
uint32_t time_estimate_pbkdf2_ms(uint32_t iterations) {
|
||||
#if EMULATOR
|
||||
(void)iterations;
|
||||
return 500;
|
||||
#else
|
||||
return PIN_PBKDF2_CYCLES_PER_ITER * iterations / (MCU_CLOCK / 1000);
|
||||
#endif
|
||||
}
|
27
legacy/time_estimate.h
Normal file
27
legacy/time_estimate.h
Normal file
@ -0,0 +1,27 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef TREZORHAL_TIME_ESTIMATE_H
|
||||
#define TREZORHAL_TIME_ESTIMATE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
uint32_t time_estimate_pbkdf2_ms(uint32_t iterations);
|
||||
|
||||
#endif
|
@ -31,6 +31,7 @@
|
||||
#include "sha2.h"
|
||||
#include "storage.h"
|
||||
#include "storage_utils.h"
|
||||
#include "time_estimate.h"
|
||||
|
||||
#if USE_OPTIGA
|
||||
#include "optiga.h"
|
||||
@ -88,22 +89,8 @@ const uint32_t V0_PIN_EMPTY = 1;
|
||||
// The total number of iterations to use in PBKDF2.
|
||||
#define PIN_ITER_COUNT 20000
|
||||
|
||||
// The number of milliseconds required to execute PBKDF2.
|
||||
#define PIN_PBKDF2_MS 1280
|
||||
|
||||
// The number of milliseconds required to set the PIN.
|
||||
#if USE_OPTIGA
|
||||
#define PIN_SET_MS (PIN_PBKDF2_MS + OPTIGA_PIN_SET_MS)
|
||||
#else
|
||||
#define PIN_SET_MS PIN_PBKDF2_MS
|
||||
#endif
|
||||
|
||||
// The number of milliseconds required to verify the PIN.
|
||||
#if USE_OPTIGA
|
||||
#define PIN_VERIFY_MS (PIN_PBKDF2_MS + OPTIGA_PIN_VERIFY_MS)
|
||||
#else
|
||||
#define PIN_VERIFY_MS PIN_PBKDF2_MS
|
||||
#endif
|
||||
// The minimum number of milliseconds between progress updates.
|
||||
#define MIN_PROGRESS_UPDATE_MS 100
|
||||
|
||||
// The length of the hashed hardware salt in bytes.
|
||||
#define HARDWARE_SALT_SIZE SHA256_DIGEST_LENGTH
|
||||
@ -152,7 +139,8 @@ CONFIDENTIAL static secbool initialized = secfalse;
|
||||
CONFIDENTIAL static secbool unlocked = secfalse;
|
||||
static PIN_UI_WAIT_CALLBACK ui_callback = NULL;
|
||||
static uint32_t ui_total = 0;
|
||||
static uint32_t ui_rem = 0;
|
||||
static uint32_t ui_begin = 0;
|
||||
static uint32_t ui_next_update = 0;
|
||||
static enum storage_ui_message_t ui_message = NO_MSG;
|
||||
CONFIDENTIAL static uint8_t cached_keys[KEYS_SIZE] = {0};
|
||||
CONFIDENTIAL static uint8_t *const cached_dek = cached_keys;
|
||||
@ -458,31 +446,83 @@ static secbool is_not_wipe_code(const uint8_t *pin, size_t pin_len) {
|
||||
return sectrue;
|
||||
}
|
||||
|
||||
static void ui_total_init(uint32_t total_ms) {
|
||||
ui_total = total_ms;
|
||||
ui_rem = total_ms;
|
||||
static uint32_t ui_estimate_time_ms(storage_pin_op_t op) {
|
||||
uint32_t time_ms = 0;
|
||||
#if USE_OPTIGA
|
||||
time_ms += optiga_estimate_time_ms(op);
|
||||
#endif
|
||||
|
||||
uint32_t pbkdf2_ms = time_estimate_pbkdf2_ms(PIN_ITER_COUNT);
|
||||
switch (op) {
|
||||
case STORAGE_PIN_OP_SET:
|
||||
case STORAGE_PIN_OP_VERIFY:
|
||||
time_ms += pbkdf2_ms;
|
||||
break;
|
||||
case STORAGE_PIN_OP_CHANGE:
|
||||
time_ms += 2 * pbkdf2_ms;
|
||||
break;
|
||||
default:
|
||||
return 1;
|
||||
}
|
||||
|
||||
return time_ms;
|
||||
}
|
||||
|
||||
static void ui_total_add(uint32_t added_ms) {
|
||||
ui_total += added_ms;
|
||||
ui_rem += added_ms;
|
||||
static void ui_progress_init(storage_pin_op_t op) {
|
||||
ui_total = ui_estimate_time_ms(op);
|
||||
ui_next_update = 0;
|
||||
}
|
||||
|
||||
static secbool ui_progress(uint32_t elapsed_ms) {
|
||||
ui_rem -= elapsed_ms;
|
||||
if (ui_callback && ui_message) {
|
||||
uint32_t progress = 0;
|
||||
if (ui_total < 1000000) {
|
||||
progress = 1000 * (ui_total - ui_rem) / ui_total;
|
||||
} else {
|
||||
// Avoid overflow. Precise enough.
|
||||
progress = (ui_total - ui_rem) / (ui_total / 1000);
|
||||
}
|
||||
// Round the remaining time to the nearest second.
|
||||
return ui_callback((ui_rem + 500) / 1000, progress, ui_message);
|
||||
} else {
|
||||
static void ui_progress_add(uint32_t added_ms) { ui_total += added_ms; }
|
||||
|
||||
static secbool ui_progress(void) {
|
||||
uint32_t now = hal_ticks_ms();
|
||||
if (ui_callback == NULL || ui_message == 0 || now < ui_next_update) {
|
||||
return secfalse;
|
||||
}
|
||||
|
||||
// The UI dialog is initialized by calling ui_callback() with progress = 0. If
|
||||
// this is the first call, i.e. ui_next_update == 0, then make sure that
|
||||
// progress comes out exactly 0.
|
||||
if (ui_next_update == 0) {
|
||||
ui_begin = now;
|
||||
}
|
||||
ui_next_update = now + MIN_PROGRESS_UPDATE_MS;
|
||||
uint32_t ui_elapsed = now - ui_begin;
|
||||
|
||||
// Round the remaining time to the nearest second.
|
||||
uint32_t ui_rem_sec = (ui_total - ui_elapsed + 500) / 1000;
|
||||
|
||||
#ifndef TREZOR_EMULATOR
|
||||
uint32_t progress = 0;
|
||||
if (ui_total < 1000000) {
|
||||
progress = 1000 * ui_elapsed / ui_total;
|
||||
} else {
|
||||
// Avoid uint32 overflow. Precise enough.
|
||||
progress = ui_elapsed / (ui_total / 1000);
|
||||
}
|
||||
#else
|
||||
// In the emulator we derive the progress from the number of remaining seconds
|
||||
// to avoid flaky UI tests.
|
||||
uint32_t ui_total_sec = (ui_total + 500) / 1000;
|
||||
uint32_t progress = 1000 - 1000 * ui_rem_sec / ui_total_sec;
|
||||
#endif
|
||||
|
||||
// Avoid reaching progress = 1000 or overflowing the total time, since calling
|
||||
// ui_callback() with progress = 1000 terminates the UI dialog.
|
||||
if (progress >= 1000) {
|
||||
progress = 999;
|
||||
ui_elapsed = ui_total;
|
||||
}
|
||||
|
||||
return ui_callback(ui_rem_sec, progress, ui_message);
|
||||
}
|
||||
|
||||
static void ui_progress_finish(void) {
|
||||
// The UI dialog is terminated by calling ui_callback() with progress = 1000.
|
||||
if (ui_callback != NULL && ui_message != 0) {
|
||||
ui_callback(0, 1000, ui_message);
|
||||
}
|
||||
}
|
||||
|
||||
#if !USE_OPTIGA
|
||||
@ -510,7 +550,7 @@ static void derive_kek_v4(const uint8_t *pin, size_t pin_len,
|
||||
pbkdf2_hmac_sha256_Init(&ctx, pin, pin_len, salt, salt_len, 1);
|
||||
for (int i = 1; i <= 5; i++) {
|
||||
pbkdf2_hmac_sha256_Update(&ctx, PIN_ITER_COUNT / 10);
|
||||
ui_progress(PIN_PBKDF2_MS / 10);
|
||||
ui_progress();
|
||||
}
|
||||
|
||||
#ifdef STM32U5
|
||||
@ -527,7 +567,7 @@ static void derive_kek_v4(const uint8_t *pin, size_t pin_len,
|
||||
pbkdf2_hmac_sha256_Init(&ctx, pin, pin_len, salt, salt_len, 2);
|
||||
for (int i = 6; i <= 10; i++) {
|
||||
pbkdf2_hmac_sha256_Update(&ctx, PIN_ITER_COUNT / 10);
|
||||
ui_progress(PIN_PBKDF2_MS / 10);
|
||||
ui_progress();
|
||||
}
|
||||
pbkdf2_hmac_sha256_Final(&ctx, keiv);
|
||||
|
||||
@ -569,7 +609,7 @@ static void stretch_pin(const uint8_t *pin, size_t pin_len,
|
||||
|
||||
for (int i = 1; i <= 10; i++) {
|
||||
pbkdf2_hmac_sha256_Update(&ctx, PIN_ITER_COUNT / 10);
|
||||
ui_progress(PIN_PBKDF2_MS / 10);
|
||||
ui_progress();
|
||||
}
|
||||
#ifdef STM32U5
|
||||
uint8_t stretched_pin_tmp[SHA256_DIGEST_LENGTH] = {0};
|
||||
@ -683,7 +723,6 @@ static secbool set_pin(const uint8_t *pin, size_t pin_len,
|
||||
uint8_t keiv[12] = {0};
|
||||
chacha20poly1305_ctx ctx = {0};
|
||||
random_buffer(rand_salt, STORAGE_SALT_SIZE);
|
||||
ui_progress(0);
|
||||
ensure(derive_kek_set(pin, pin_len, rand_salt, ext_salt, kek),
|
||||
"derive_kek_set failed");
|
||||
rfc7539_init(&ctx, kek, keiv);
|
||||
@ -739,9 +778,10 @@ static void init_wiped_storage(void) {
|
||||
ensure(set_wipe_code(WIPE_CODE_EMPTY, WIPE_CODE_EMPTY_LEN),
|
||||
"set_wipe_code failed");
|
||||
|
||||
ui_total_init(PIN_SET_MS);
|
||||
ui_progress_init(STORAGE_PIN_OP_SET);
|
||||
ui_message = PROCESSING_MSG;
|
||||
ensure(set_pin(PIN_EMPTY, PIN_EMPTY_LEN, NULL), "init_pin failed");
|
||||
ui_progress_finish();
|
||||
}
|
||||
|
||||
void storage_init(PIN_UI_WAIT_CALLBACK callback, const uint8_t *salt,
|
||||
@ -938,7 +978,7 @@ static secbool unlock(const uint8_t *pin, size_t pin_len,
|
||||
// In case of an upgrade from version 4 or earlier bump the total time of UI
|
||||
// progress to account for the set_pin() call in storage_upgrade_unlocked().
|
||||
if (get_lock_version() <= 4) {
|
||||
ui_total_add(PIN_SET_MS);
|
||||
ui_progress_add(ui_estimate_time_ms(STORAGE_PIN_OP_SET));
|
||||
}
|
||||
|
||||
// Now we can check for wipe code.
|
||||
@ -960,11 +1000,13 @@ static secbool unlock(const uint8_t *pin, size_t pin_len,
|
||||
}
|
||||
|
||||
// Sleep for 2^ctr - 1 seconds before checking the PIN.
|
||||
uint32_t wait = (1 << ctr) - 1;
|
||||
ui_total_add(wait * 1000);
|
||||
ui_progress(0);
|
||||
for (uint32_t i = 0; i < 10 * wait; i++) {
|
||||
if (sectrue == ui_progress(100)) {
|
||||
uint32_t wait_ms = 1000 * ((1 << ctr) - 1);
|
||||
ui_progress_add(wait_ms);
|
||||
ui_progress();
|
||||
|
||||
uint32_t begin = hal_ticks_ms();
|
||||
while (hal_ticks_ms() - begin < wait_ms) {
|
||||
if (sectrue == ui_progress()) {
|
||||
memzero(&legacy_pin, sizeof(legacy_pin));
|
||||
return secfalse;
|
||||
}
|
||||
@ -995,10 +1037,10 @@ static secbool unlock(const uint8_t *pin, size_t pin_len,
|
||||
show_pin_too_many_screen();
|
||||
}
|
||||
|
||||
// Finish the countdown. Check for ui_rem underflow.
|
||||
while (0 < ui_rem && ui_rem < ui_total) {
|
||||
// Finish the countdown.
|
||||
while (hal_ticks_ms() - ui_begin < ui_total) {
|
||||
ui_message = WRONG_PIN_MSG;
|
||||
if (sectrue == ui_progress(100)) {
|
||||
if (sectrue == ui_progress()) {
|
||||
return secfalse;
|
||||
}
|
||||
hal_delay(100);
|
||||
@ -1030,7 +1072,7 @@ secbool storage_unlock(const uint8_t *pin, size_t pin_len,
|
||||
return secfalse;
|
||||
}
|
||||
|
||||
ui_total_init(PIN_VERIFY_MS);
|
||||
ui_progress_init(STORAGE_PIN_OP_VERIFY);
|
||||
if (pin_len == 0) {
|
||||
if (ui_message == NO_MSG) {
|
||||
ui_message = STARTING_MSG;
|
||||
@ -1040,7 +1082,10 @@ secbool storage_unlock(const uint8_t *pin, size_t pin_len,
|
||||
} else {
|
||||
ui_message = VERIFYING_PIN_MSG;
|
||||
}
|
||||
return unlock(pin, pin_len, ext_salt);
|
||||
|
||||
secbool ret = unlock(pin, pin_len, ext_salt);
|
||||
ui_progress_finish();
|
||||
return ret;
|
||||
}
|
||||
|
||||
/*
|
||||
@ -1326,20 +1371,26 @@ secbool storage_change_pin(const uint8_t *oldpin, size_t oldpin_len,
|
||||
return secfalse;
|
||||
}
|
||||
|
||||
ui_total_init(PIN_VERIFY_MS + PIN_SET_MS);
|
||||
ui_progress_init(STORAGE_PIN_OP_CHANGE);
|
||||
ui_message =
|
||||
(oldpin_len != 0 && newpin_len == 0) ? VERIFYING_PIN_MSG : PROCESSING_MSG;
|
||||
|
||||
if (sectrue != unlock(oldpin, oldpin_len, old_ext_salt)) {
|
||||
return secfalse;
|
||||
secbool ret = unlock(oldpin, oldpin_len, old_ext_salt);
|
||||
if (sectrue != ret) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
// Fail if the new PIN is the same as the wipe code.
|
||||
if (sectrue != is_not_wipe_code(newpin, newpin_len)) {
|
||||
return secfalse;
|
||||
ret = is_not_wipe_code(newpin, newpin_len);
|
||||
if (sectrue != ret) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
return set_pin(newpin, newpin_len, new_ext_salt);
|
||||
ret = set_pin(newpin, newpin_len, new_ext_salt);
|
||||
|
||||
end:
|
||||
ui_progress_finish();
|
||||
return ret;
|
||||
}
|
||||
|
||||
void storage_ensure_not_wipe_code(const uint8_t *pin, size_t pin_len) {
|
||||
@ -1374,14 +1425,19 @@ secbool storage_change_wipe_code(const uint8_t *pin, size_t pin_len,
|
||||
return secfalse;
|
||||
}
|
||||
|
||||
ui_total_init(PIN_VERIFY_MS);
|
||||
ui_progress_init(STORAGE_PIN_OP_VERIFY);
|
||||
ui_message =
|
||||
(pin_len != 0 && wipe_code_len == 0) ? VERIFYING_PIN_MSG : PROCESSING_MSG;
|
||||
|
||||
secbool ret = secfalse;
|
||||
if (sectrue == unlock(pin, pin_len, ext_salt)) {
|
||||
ret = set_wipe_code(wipe_code, wipe_code_len);
|
||||
secbool ret = unlock(pin, pin_len, ext_salt);
|
||||
if (sectrue != ret) {
|
||||
goto end;
|
||||
}
|
||||
|
||||
ret = set_wipe_code(wipe_code, wipe_code_len);
|
||||
|
||||
end:
|
||||
ui_progress_finish();
|
||||
return ret;
|
||||
}
|
||||
|
||||
@ -1550,15 +1606,17 @@ static secbool storage_upgrade(void) {
|
||||
}
|
||||
|
||||
// Set EDEK_PVC_KEY and PIN_NOT_SET_KEY.
|
||||
ui_total_init(PIN_SET_MS);
|
||||
ui_message = PROCESSING_MSG;
|
||||
uint8_t pin[V0_MAX_PIN_LEN] = {0};
|
||||
size_t pin_len = 0;
|
||||
secbool found = norcow_get(V0_PIN_KEY, &val, &len);
|
||||
if (sectrue == found && *(const uint32_t *)val != V0_PIN_EMPTY) {
|
||||
pin_len = int_to_pin(*(const uint32_t *)val, pin);
|
||||
}
|
||||
|
||||
ui_progress_init(STORAGE_PIN_OP_SET);
|
||||
ui_message = PROCESSING_MSG;
|
||||
set_pin(pin, pin_len, NULL);
|
||||
ui_progress_finish();
|
||||
memzero(pin, sizeof(pin));
|
||||
|
||||
// Convert PIN failure counter.
|
||||
|
@ -61,6 +61,12 @@ enum storage_ui_message_t {
|
||||
WRONG_PIN_MSG,
|
||||
};
|
||||
|
||||
typedef enum {
|
||||
STORAGE_PIN_OP_SET = 0,
|
||||
STORAGE_PIN_OP_VERIFY,
|
||||
STORAGE_PIN_OP_CHANGE,
|
||||
} storage_pin_op_t;
|
||||
|
||||
typedef secbool (*PIN_UI_WAIT_CALLBACK)(uint32_t wait, uint32_t progress,
|
||||
enum storage_ui_message_t message);
|
||||
|
||||
|
@ -4,6 +4,7 @@ CFLAGS = -Wall -Wshadow -Wextra -Wpedantic -Werror -Wno-missing-braces
|
||||
CFLAGS += -fPIC
|
||||
CFALGS += -fsanitize=address,undefined
|
||||
CFLAGS += -DTREZOR_MODEL_T
|
||||
CFLAGS += -DTREZOR_EMULATOR
|
||||
CFLAGS += -DUSE_INSECURE_PRNG
|
||||
CFLAGS += -DCONFIDENTIAL=""
|
||||
|
||||
|
@ -23,6 +23,8 @@
|
||||
|
||||
#include "common.h"
|
||||
|
||||
static uint32_t ticks_ms = 0;
|
||||
|
||||
void __shutdown(void) {
|
||||
printf("SHUTDOWN\n");
|
||||
exit(3);
|
||||
@ -41,3 +43,6 @@ void __fatal_error(const char *msg, const char *file, int line) {
|
||||
|
||||
void show_wipe_code_screen(void) {}
|
||||
void show_pin_too_many_screen(void) {}
|
||||
|
||||
void hal_delay(uint32_t delay_ms) { ticks_ms += delay_ms; }
|
||||
uint32_t hal_ticks_ms(void) { return ticks_ms; }
|
||||
|
@ -30,6 +30,7 @@ void show_pin_too_many_screen(void);
|
||||
#define ensure(expr, msg) \
|
||||
(((expr) == sectrue) ? (void)0 : __fatal_error(msg, __FILE__, __LINE__))
|
||||
|
||||
#define hal_delay(ms) (void)ms;
|
||||
void hal_delay(uint32_t delay_ms);
|
||||
uint32_t hal_ticks_ms(void);
|
||||
|
||||
#endif
|
||||
|
30
storage/tests/c/time_estimate.h
Normal file
30
storage/tests/c/time_estimate.h
Normal file
@ -0,0 +1,30 @@
|
||||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
|
||||
#ifndef TREZORHAL_TIME_ESTIMATE_H
|
||||
#define TREZORHAL_TIME_ESTIMATE_H
|
||||
|
||||
#include <stdint.h>
|
||||
|
||||
uint32_t time_estimate_pbkdf2_ms(uint32_t iterations) {
|
||||
(void)iterations;
|
||||
return 500;
|
||||
}
|
||||
|
||||
#endif
|
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user