SFT-6378: responded to more feedback, needs testing and evaluation
What changed, and why it matters
This commit hardens the code that converts a user's BIP39 recovery phrase (seed words) into secret bits. It fixes a bug where 8-character words were wrongly rejected, makes the loop length independent of the number of words to reduce timing clues, adds bounds checks to prevent reading past the end of the input buffer, and clears sensitive buffers before returning on error. The changes are defensive and reduce side-channel and out-of-bounds risks, but the commit message frames them as feedback responses needing testing, so it is not a finished security fix.
Treat this as a defensive hardening change that should be reviewed and tested before release. Verify that the constant-time properties hold across the compiler and target hardware, confirm the bounds checks prevent all out-of-bounds reads, and ensure the removal of the `bi != n * 11` check does not introduce a length-validation gap. Consider whether additional clearing of the output `bits` buffer is needed on failure paths.
Security signals we found
Timing-side-channel mitigation: outer loop iteration count made independent of mnemonic word count
Out-of-bounds read prevention: inner loop index bounded against sizeof(padded)-1
Off-by-one fix: inner loop bound changed from BIP39_MAX_WORD_LEN-1 to BIP39_MAX_WORD_LEN
Sensitive-buffer zeroization on error paths (padded, current_word, result)
Masking of dummy-word contributions to result bits and found-word check
Removal of final bi != n*11 length check as redundant with fixed loop
Evidence from the diff
The patch modifies mnemonic_to_bits() in extmod/trezor-firmware/crypto/bip39.c. Key changes: (1) outer loop now always iterates BIP39_MNEMONIC_MAX_WORDS (24) times with a mask (active) so dummy iterations do not affect results, mitigating word-count timing leakage; (2) inner loop corrected from BIP39_MAX_WORD_LEN - 1 to BIP39_MAX_WORD_LEN and reads are bounds-checked against sizeof(padded) - 1, fixing an off-by-one that caused 8-character words to set word_too_long and preventing out-of-bounds reads on overlong crafted input; (3) all_words_found and bit writes are masked by active; (4) error paths now memzero padded, current_word, and result; (5) the final bi != n * 11 check is removed because the fixed loop makes it unnecessary. The commit title says ‘needs testing and evaluation’, indicating it is a work-in-progress hardening patch rather than a confirmed vulnerability fix.
Changed components
extmod/trezor-firmware/crypto/bip39.cmnemonic_to_bits() functionInspect captured patch +25 / −18
diff --git a/extmod/trezor-firmware/crypto/bip39.c b/extmod/trezor-firmware/crypto/bip39.c
index df12c43..69a458e 100644
--- a/extmod/trezor-firmware/crypto/bip39.c
+++ b/extmod/trezor-firmware/crypto/bip39.c
@@ -156,6 +156,7 @@ int mnemonic_to_bits(const char* mnemonic, uint8_t* bits) {
// (b) number of bits divisible by 33 (1 checksum bit per 32 input bits)
// - that is, (n * 11) % 33 == 0, so n % 3 == 0
if (n < 12 || n > 24 || (n % 3)) {
+ memzero(padded, sizeof(padded));
return 0;
}
@@ -167,16 +168,22 @@ int mnemonic_to_bits(const char* mnemonic, uint8_t* bits) {
memzero(result, sizeof(result));
i = 0;
- for (uint32_t w = 0; w < n; w++) {
+ // Outer loop always runs BIP39_MNEMONIC_MAX_WORDS (24) iterations so that
+ // the word count n does not influence total iteration count (timing).
+ // Dummy iterations (w >= n) are masked out and do not affect the result.
+ for (uint32_t w = 0; w < BIP39_MNEMONIC_MAX_WORDS; w++) {
+ // active is 0xFFFFFFFF for real words (w < n), 0 for dummy iterations.
+ uint32_t active = -(uint32_t)(w < n);
j = 0;
memzero(current_word, sizeof(current_word));
- // Fixed inner loop: always BIP39_MAX_WORD_LEN - 1 iterations.
- // Uses a latching past_delim flag to suppress copies after the word
- // boundary instead of breaking early on input-dependent data.
+ // Fixed inner loop: always BIP39_MAX_WORD_LEN iterations (was -1, which
+ // caused 8-char words to never set past_delim → spurious word_too_long).
+ // Each read is bounds-checked to prevent OOB on crafted overlong input.
uint32_t past_delim = 0;
- for (uint32_t ci = 0; ci < BIP39_MAX_WORD_LEN - 1; ci++) {
- char c = padded[i + ci];
+ for (uint32_t ci = 0; ci < BIP39_MAX_WORD_LEN; ci++) {
+ uint32_t idx = i + ci;
+ char c = (idx < (uint32_t)(sizeof(padded) - 1)) ? padded[idx] : '\0';
uint32_t is_delim = (uint32_t)((c == ' ') | (c == '\0'));
past_delim |= is_delim;
if (!past_delim) {
@@ -187,18 +194,18 @@ int mnemonic_to_bits(const char* mnemonic, uint8_t* bits) {
// Advance i past the characters copied into current_word
i += j;
- // If past_delim was never set the word overruns BIP39_MAX_WORD_LEN - 1;
+ // If past_delim was never set the word overruns BIP39_MAX_WORD_LEN;
// skip remaining characters. Valid mnemonics never take this path.
if (!past_delim) {
word_too_long = 1;
- while (i < BIP39_MNEMONIC_MAX_WORDS * BIP39_MAX_WORD_LEN &&
+ while (i < (uint32_t)(sizeof(padded) - 1) &&
padded[i] != ' ' && padded[i] != '\0') {
i++;
}
}
// Skip the word delimiter (space) if present
- if (i < BIP39_MNEMONIC_MAX_WORDS * BIP39_MAX_WORD_LEN && padded[i] == ' ') {
+ if (i < (uint32_t)(sizeof(padded) - 1) && padded[i] == ' ') {
i++;
}
@@ -215,26 +222,26 @@ int mnemonic_to_bits(const char* mnemonic, uint8_t* bits) {
found |= mask;
}
- // Track if this word was found (constant-time accumulation)
- all_words_found &= found;
+ // Only require found for active (non-dummy) words.
+ all_words_found &= (found | ~active);
- // Constant-time bit extraction and setting
- // Always execute all 11 iterations, no conditional branching on bit values
+ // Constant-time bit extraction; mask out dummy-word contributions so
+ // they do not alter result bits beyond the n*11 active bits.
for (ki = 0; ki < 11; ki++) {
- uint8_t bit = (found_index >> (10 - ki)) & 1;
- result[bi / 8] |= bit << (7 - (bi % 8));
+ uint8_t bit = (uint8_t)((found_index >> (10 - ki)) & 1);
+ result[bi / 8] |= (bit << (7 - (bi % 8))) & (uint8_t)(active & 0xFFu);
bi++;
}
}
// Check all words were found and no word exceeded the maximum length
if (all_words_found == 0 || word_too_long) {
+ memzero(padded, sizeof(padded));
+ memzero(current_word, sizeof(current_word));
+ memzero(result, sizeof(result));
return 0;
}
- if (bi != n * 11) {
- return 0;
- }
memcpy(bits, result, sizeof(result));
memzero(result, sizeof(result));
memzero(current_word, sizeof(current_word));
Why this scored 63/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.