Skip to content

Commit 5ec5d99

Browse files
committed
rust/keystore: make bip39 unlocking async
This uses `to_seed_normalized_async(...).await` over `to_seed_normalized(...)` in bip39 unlocking, propagating the async/await keywords up the stack. This commit by itself is not functional yet, as the unlock animation is still timer-interrupt based, which leads to chaos. The next commit converts the animation into an async task of its own, not depending on interrupts. The bip39 unlock loop is made to yield to the executor in each of the 2048 PBKDF2 stretch rounds. In the simulator however, we don't yield and finish the computation in a blocking fashion like before, due to a limitation of the simulator: it does not busy-loop the mainloop (otherwise CPU would be at 100%), but only when there is an incoming USB packet, so yielding in BIP39 would make unlocking in the simulator *very* slow. Running the mainloop quicker in the simulator does not work well: either CPU load is too high, or unlock is too slow.
1 parent dc6934f commit 5ec5d99

File tree

16 files changed

+331
-109
lines changed

16 files changed

+331
-109
lines changed

src/CMakeLists.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,6 @@ set(DBB-FIRMWARE-USB-SOURCES ${DBB-FIRMWARE-USB-SOURCES} PARENT_SCOPE)
5353
set(DBB-FIRMWARE-UI-SOURCES
5454
${CMAKE_SOURCE_DIR}/src/screen.c
5555
${CMAKE_SOURCE_DIR}/src/ui/graphics/graphics.c
56-
${CMAKE_SOURCE_DIR}/src/ui/graphics/lock_animation.c
5756
${CMAKE_SOURCE_DIR}/src/ui/ugui/ugui.c
5857
${CMAKE_SOURCE_DIR}/src/ui/fonts/font_a_9X9.c
5958
${CMAKE_SOURCE_DIR}/src/ui/fonts/font_a_11X10.c
@@ -85,6 +84,7 @@ set(DBB-FIRMWARE-UI-SOURCES
8584
${CMAKE_SOURCE_DIR}/src/ui/components/orientation_arrows.c
8685
${CMAKE_SOURCE_DIR}/src/ui/components/info_centered.c
8786
${CMAKE_SOURCE_DIR}/src/ui/components/lockscreen.c
87+
${CMAKE_SOURCE_DIR}/src/ui/components/unlock_animation.c
8888
${CMAKE_SOURCE_DIR}/src/ui/components/menu.c
8989
${CMAKE_SOURCE_DIR}/src/ui/components/status.c
9090
${CMAKE_SOURCE_DIR}/src/ui/components/image.c

src/rust/bitbox02-rust/src/keystore.rs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,7 @@ mod tests {
270270
use bitbox02::testing::{
271271
TEST_MNEMONIC, mock_memory, mock_unlocked, mock_unlocked_using_mnemonic,
272272
};
273+
use util::bb02_async::block_on;
273274

274275
use bitcoin::secp256k1;
275276

@@ -573,7 +574,15 @@ mod tests {
573574
keystore::lock();
574575
let seed = &seed[..test.seed_len];
575576

576-
assert!(keystore::unlock_bip39(SECP256K1, seed, test.mnemonic_passphrase).is_err());
577+
assert!(
578+
block_on(keystore::unlock_bip39(
579+
SECP256K1,
580+
seed,
581+
test.mnemonic_passphrase,
582+
async || {}
583+
))
584+
.is_err()
585+
);
577586

578587
bitbox02::securechip::fake_event_counter_reset();
579588
assert!(keystore::encrypt_and_store_seed(seed, "foo").is_ok());
@@ -582,7 +591,15 @@ mod tests {
582591
assert!(keystore::is_locked());
583592

584593
bitbox02::securechip::fake_event_counter_reset();
585-
assert!(keystore::unlock_bip39(SECP256K1, seed, test.mnemonic_passphrase).is_ok());
594+
assert!(
595+
block_on(keystore::unlock_bip39(
596+
SECP256K1,
597+
seed,
598+
test.mnemonic_passphrase,
599+
async || {}
600+
))
601+
.is_ok()
602+
);
586603
assert_eq!(bitbox02::securechip::fake_event_counter(), 1);
587604

588605
assert!(!keystore::is_locked());

src/rust/bitbox02-rust/src/workflow.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ pub mod transaction;
2727
pub mod trinary_choice;
2828
pub mod trinary_input_string;
2929
pub mod unlock;
30+
pub mod unlock_animation;
3031
pub mod verify_message;
3132

3233
use alloc::string::String;

src/rust/bitbox02-rust/src/workflow/unlock.rs

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -133,9 +133,23 @@ pub async fn unlock_bip39(hal: &mut impl crate::hal::Hal, seed: &[u8]) {
133133
}
134134
}
135135

136-
let result = bitbox02::ui::with_lock_animation(|| {
137-
keystore::unlock_bip39(crate::secp256k1::SECP256K1, seed, &mnemonic_passphrase)
138-
});
136+
let ((), result) = util::bb02_async::join(
137+
super::unlock_animation::animate(),
138+
keystore::unlock_bip39(
139+
crate::secp256k1::SECP256K1,
140+
seed,
141+
&mnemonic_passphrase,
142+
// for the simulator, we don't yield at all, otherwise unlock becomes very slow in the
143+
// simulator.
144+
#[cfg(feature = "c-unit-testing")]
145+
async || {},
146+
// we yield every time to keep the processing time per iteration to a minimum.
147+
#[cfg(not(feature = "c-unit-testing"))]
148+
util::bb02_async::yield_now,
149+
),
150+
)
151+
.await;
152+
139153
if result.is_err() {
140154
abort("bip39 unlock failed");
141155
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
// Copyright 2025 Shift Crypto AG
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use crate::bb02_async::option_no_screensaver;
16+
use core::cell::RefCell;
17+
18+
/// Performs the unlock animation. Its duration is determined by the component render rate, see
19+
/// unlock_animation.c
20+
pub async fn animate() {
21+
let result = RefCell::new(None as Option<()>);
22+
let mut component = bitbox02::ui::unlock_animation_create(|| {
23+
*result.borrow_mut() = Some(());
24+
});
25+
component.screen_stack_push();
26+
option_no_screensaver(&result).await
27+
}

src/rust/bitbox02-sys/build.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ const ALLOWLIST_FNS: &[&str] = &[
6767
"delay_ms",
6868
"delay_us",
6969
"empty_create",
70+
"unlock_animation_create",
7071
"keystore_bip39_mnemonic_to_seed",
7172
"keystore_copy_seed",
7273
"keystore_copy_bip39_seed",
@@ -85,8 +86,6 @@ const ALLOWLIST_FNS: &[&str] = &[
8586
"keystore_test_get_retained_bip39_seed_encrypted",
8687
"label_create",
8788
"localtime",
88-
"lock_animation_start",
89-
"lock_animation_stop",
9089
"memory_set_salt_root",
9190
"memory_add_noise_remote_static_pubkey",
9291
"memory_bootloader_hash",
@@ -212,6 +211,7 @@ const BITBOX02_SOURCES: &[&str] = &[
212211
"src/ui/components/label.c",
213212
"src/ui/components/left_arrow.c",
214213
"src/ui/components/lockscreen.c",
214+
"src/ui/components/unlock_animation.c",
215215
"src/ui/components/menu.c",
216216
"src/ui/components/orientation_arrows.c",
217217
"src/ui/components/progress.c",
@@ -235,7 +235,6 @@ const BITBOX02_SOURCES: &[&str] = &[
235235
"src/ui/fonts/password_11X12.c",
236236
"src/ui/fonts/password_9X9.c",
237237
"src/ui/graphics/graphics.c",
238-
"src/ui/graphics/lock_animation.c",
239238
"src/ui/oled/sh1107.c",
240239
"src/ui/oled/ssd1312.c",
241240
"src/ui/screen_process.c",

src/rust/bitbox02-sys/wrapper.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,11 @@
3939
#include <ui/components/status.h>
4040
#include <ui/components/trinary_choice.h>
4141
#include <ui/components/trinary_input_string.h>
42+
#include <ui/components/unlock_animation.h>
4243
#include <ui/fonts/font_a_11X10.h>
4344
#include <ui/fonts/font_a_9X9.h>
4445
#include <ui/fonts/monogram_5X9.h>
4546
#include <ui/fonts/password_11X12.h>
46-
#include <ui/graphics/lock_animation.h>
4747
#include <ui/oled/oled.h>
4848
#include <ui/screen_process.h>
4949
#include <ui/screen_saver.h>

src/rust/bitbox02/src/keystore.rs

Lines changed: 37 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -111,14 +111,18 @@ fn unlock_bip39_finalize(bip39_seed: &[u8; 64]) -> Result<(), Error> {
111111
}
112112
}
113113

114-
fn derive_bip39_seed(
114+
async fn derive_bip39_seed(
115115
secp: &Secp256k1<All>,
116116
seed: &[u8],
117117
mnemonic_passphrase: &str,
118+
yield_now: impl AsyncFn(),
118119
) -> (zeroize::Zeroizing<[u8; 64]>, [u8; 4]) {
119120
let mnemonic = bip39::Mnemonic::from_entropy_in(bip39::Language::English, seed).unwrap();
120-
let bip39_seed: zeroize::Zeroizing<[u8; 64]> =
121-
zeroize::Zeroizing::new(mnemonic.to_seed_normalized(mnemonic_passphrase));
121+
let bip39_seed: zeroize::Zeroizing<[u8; 64]> = zeroize::Zeroizing::new(
122+
mnemonic
123+
.to_seed_normalized_async(mnemonic_passphrase, yield_now)
124+
.await,
125+
);
122126
let root_fingerprint: [u8; 4] =
123127
bitcoin::bip32::Xpriv::new_master(bitcoin::NetworkKind::Main, bip39_seed.as_ref())
124128
.unwrap()
@@ -132,14 +136,16 @@ fn derive_bip39_seed(
132136
/// of `keystore_copy_seed()`).
133137
/// `mnemonic_passphrase` is the bip39 passphrase used in the derivation. Use the empty string if no
134138
/// passphrase is needed or provided.
135-
pub fn unlock_bip39(
139+
pub async fn unlock_bip39(
136140
secp: &Secp256k1<All>,
137141
seed: &[u8],
138142
mnemonic_passphrase: &str,
143+
yield_now: impl AsyncFn(),
139144
) -> Result<(), Error> {
140145
unlock_bip39_check(seed)?;
141146

142-
let (bip39_seed, root_fingerprint) = derive_bip39_seed(secp, seed, mnemonic_passphrase);
147+
let (bip39_seed, root_fingerprint) =
148+
derive_bip39_seed(secp, seed, mnemonic_passphrase, yield_now).await;
143149

144150
unlock_bip39_finalize(bip39_seed.as_slice().try_into().unwrap())?;
145151

@@ -279,6 +285,7 @@ mod tests {
279285
use bitcoin::secp256k1;
280286

281287
use crate::testing::{mock_memory, mock_unlocked_using_mnemonic};
288+
use util::bb02_async::block_on;
282289

283290
#[test]
284291
fn test_secp256k1_sign() {
@@ -442,7 +449,15 @@ mod tests {
442449
.unwrap();
443450
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
444451
assert!(is_locked()); // still locked, it is only unlocked after unlock_bip39.
445-
assert!(unlock_bip39(&secp256k1::Secp256k1::new(), &seed, "foo").is_ok());
452+
assert!(
453+
block_on(unlock_bip39(
454+
&secp256k1::Secp256k1::new(),
455+
&seed,
456+
"foo",
457+
async |_idx| {}
458+
))
459+
.is_ok()
460+
);
446461
assert!(!is_locked());
447462
lock();
448463
assert!(is_locked());
@@ -572,7 +587,12 @@ mod tests {
572587
let secp = secp256k1::Secp256k1::new();
573588
for test in tests {
574589
let seed = hex::decode(test.seed).unwrap();
575-
let (bip39_seed, root_fingerprint) = derive_bip39_seed(&secp, &seed, test.passphrase);
590+
let (bip39_seed, root_fingerprint) = block_on(derive_bip39_seed(
591+
&secp,
592+
&seed,
593+
test.passphrase,
594+
async |_idx| {},
595+
));
576596
assert_eq!(hex::encode(bip39_seed).as_str(), test.expected_bip39_seed);
577597
assert_eq!(
578598
hex::encode(root_fingerprint).as_str(),
@@ -600,9 +620,17 @@ mod tests {
600620
assert!(encrypt_and_store_seed(&seed, "password").is_ok());
601621
assert!(root_fingerprint().is_err());
602622
// Incorrect seed passed
603-
assert!(unlock_bip39(&secp, b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "foo").is_err());
623+
assert!(
624+
block_on(unlock_bip39(
625+
&secp,
626+
b"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
627+
"foo",
628+
async |_idx| {}
629+
))
630+
.is_err()
631+
);
604632
// Correct seed passed.
605-
assert!(unlock_bip39(&secp, &seed, "foo").is_ok());
633+
assert!(block_on(unlock_bip39(&secp, &seed, "foo", async |_idx| {})).is_ok());
606634
assert_eq!(root_fingerprint(), Ok(vec![0xf1, 0xbc, 0x3c, 0x46]),);
607635

608636
let expected_bip39_seed = hex::decode("2b3c63de86f0f2b13cc6a36c1ba2314fbc1b40c77ab9cb64e96ba4d5c62fc204748ca6626a9f035e7d431bce8c9210ec0bdffc2e7db873dee56c8ac2153eee9a").unwrap();

src/rust/bitbox02/src/testing.rs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,13 @@ pub fn mock_unlocked_using_mnemonic(mnemonic: &str, passphrase: &str) {
2222
unsafe {
2323
bitbox02_sys::keystore_mock_unlocked(seed.as_ptr(), seed.len() as _, core::ptr::null())
2424
}
25-
keystore::unlock_bip39(&bitcoin::secp256k1::Secp256k1::new(), &seed, passphrase).unwrap();
25+
util::bb02_async::block_on(keystore::unlock_bip39(
26+
&bitcoin::secp256k1::Secp256k1::new(),
27+
&seed,
28+
passphrase,
29+
async || {},
30+
))
31+
.unwrap();
2632
}
2733

2834
pub const TEST_MNEMONIC: &str = "purity concert above invest pigeon category peace tuition hazard vivid latin since legal speak nation session onion library travel spell region blast estate stay";

src/rust/bitbox02/src/ui/ui.rs

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -436,13 +436,6 @@ pub fn trinary_input_string_set_input(component: &mut Component, word: &str) {
436436
}
437437
}
438438

439-
pub fn with_lock_animation<F: Fn() -> R, R>(f: F) -> R {
440-
unsafe { bitbox02_sys::lock_animation_start() };
441-
let result = f();
442-
unsafe { bitbox02_sys::lock_animation_stop() };
443-
result
444-
}
445-
446439
pub fn screen_stack_pop_all() {
447440
unsafe {
448441
bitbox02_sys::ui_screen_stack_pop_all();
@@ -476,3 +469,31 @@ pub fn empty_create<'a>() -> Component<'a> {
476469
_p: PhantomData,
477470
}
478471
}
472+
473+
pub fn unlock_animation_create<'a, F>(on_done: F) -> Component<'a>
474+
where
475+
// Callback must outlive component.
476+
F: FnMut() + 'a,
477+
{
478+
unsafe extern "C" fn c_on_done<F2>(param: *mut c_void)
479+
where
480+
F2: FnMut(),
481+
{
482+
// The callback is dropped afterwards. This is safe because
483+
// this C callback is guaranteed to be called only once.
484+
let mut on_done = unsafe { Box::from_raw(param as *mut F2) };
485+
on_done();
486+
}
487+
let component = unsafe {
488+
bitbox02_sys::unlock_animation_create(
489+
Some(c_on_done::<F>),
490+
Box::into_raw(Box::new(on_done)) as *mut _, // passed to c_on_done as `param`.
491+
)
492+
};
493+
Component {
494+
component,
495+
is_pushed: false,
496+
on_drop: None,
497+
_p: PhantomData,
498+
}
499+
}

0 commit comments

Comments
 (0)