Skip to content

Commit 7363fde

Browse files
committed
Merge branch 'unlock-reduce-sc-events'
2 parents c5253cd + e6d8560 commit 7363fde

File tree

7 files changed

+189
-35
lines changed

7 files changed

+189
-35
lines changed

src/keystore.c

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -445,7 +445,9 @@ static bool _check_retained_seed(const uint8_t* seed, size_t seed_length)
445445
keystore_error_t keystore_unlock(
446446
const char* password,
447447
uint8_t* remaining_attempts_out,
448-
int* securechip_result_out)
448+
int* securechip_result_out,
449+
uint8_t* seed_out,
450+
size_t* seed_len_out)
449451
{
450452
if (!memory_is_seeded()) {
451453
return KEYSTORE_ERR_UNSEEDED;
@@ -486,6 +488,11 @@ keystore_error_t keystore_unlock(
486488
_is_unlocked_device = true;
487489
}
488490
bitbox02_smarteeprom_reset_unlock_attempts();
491+
492+
if (seed_out != NULL && seed_len_out != NULL) {
493+
memcpy(seed_out, seed, seed_len);
494+
*seed_len_out = seed_len;
495+
}
489496
}
490497
// Compute remaining attempts
491498
failed_attempts = bitbox02_smarteeprom_get_unlock_attempts();

src/keystore.h

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -95,14 +95,22 @@ USE_RESULT keystore_error_t keystore_create_and_store_seed(
9595
* @param[out] remaining_attempts_out will have the number of remaining attempts.
9696
* If zero, the keystore is locked until the device is reset.
9797
* @param[out] securechip_result_out, if not NULL, will contain the error code from
98+
* @param[out] seed_out The seed bytes copied from the retained seed.
99+
* The buffer should be KEYSTORE_MAX_SEED_LENGTH bytes long. The caller must
100+
* zero the seed once it is no longer needed.
101+
* @param[out] seed_len_out The seed length.
98102
* `securechip_kdf()` if there was a secure chip error, and 0 otherwise.
99103
* @return
100104
* - KEYSTORE_OK if they keystore was successfully unlocked
101105
* - KEYSTORE_ERR_* if unsuccessful.
102106
* Only call this if memory_is_seeded() returns true.
103107
*/
104-
USE_RESULT keystore_error_t
105-
keystore_unlock(const char* password, uint8_t* remaining_attempts_out, int* securechip_result_out);
108+
USE_RESULT keystore_error_t keystore_unlock(
109+
const char* password,
110+
uint8_t* remaining_attempts_out,
111+
int* securechip_result_out,
112+
uint8_t* seed_out,
113+
size_t* seed_len_out);
106114

107115
/**
108116
* Checks if bip39 unlocking can be performed. It can be performed if `keystore_unlock()`

src/rust/bitbox02-rust/src/hww/api/backup.rs

Lines changed: 73 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -99,15 +99,15 @@ pub async fn create(
9999

100100
let is_initialized = bitbox02::memory::is_initialized();
101101

102-
if is_initialized {
103-
unlock::unlock_keystore(hal, "Unlock device", unlock::CanCancel::Yes).await?;
104-
}
105-
106-
let seed = bitbox02::keystore::copy_seed()?;
107-
108-
// Yield now to give executor a chance to process USB/BLE communication, as copy_seed() causes
109-
// some delay.
110-
futures_lite::future::yield_now().await;
102+
let seed = if is_initialized {
103+
unlock::unlock_keystore(hal, "Unlock device", unlock::CanCancel::Yes).await?
104+
} else {
105+
let seed = bitbox02::keystore::copy_seed()?;
106+
// Yield now to give executor a chance to process USB/BLE communication, as copy_seed() causes
107+
// some delay.
108+
futures_lite::future::yield_now().await;
109+
seed
110+
};
111111

112112
let seed_birthdate = if !is_initialized {
113113
if bitbox02::memory::set_seed_birthdate(timestamp).is_err() {
@@ -186,6 +186,7 @@ mod tests {
186186

187187
let mut mock_hal = TestingHal::new();
188188
mock_hal.sd.inserted = Some(true);
189+
bitbox02::securechip::fake_event_counter_reset();
189190
assert_eq!(
190191
block_on(create(
191192
&mut mock_hal,
@@ -196,6 +197,7 @@ mod tests {
196197
)),
197198
Ok(Response::Success(pb::Success {}))
198199
);
200+
assert_eq!(bitbox02::securechip::fake_event_counter(), 1);
199201
assert_eq!(EXPECTED_TIMESTMAP, bitbox02::memory::get_seed_birthdate());
200202
assert_eq!(
201203
mock_hal.ui.screens,
@@ -223,6 +225,68 @@ mod tests {
223225
);
224226
}
225227

228+
/// Test backup creation on a initialized keystore. The sdcard does not contain the backup yet.
229+
#[test]
230+
pub fn test_create_initialized_new() {
231+
const TIMESTMAP: u32 = 1601281809;
232+
233+
mock_memory();
234+
235+
let seed = hex::decode("cb33c20cea62a5c277527e2002da82e6e2b37450a755143a540a54cea8da9044")
236+
.unwrap();
237+
bitbox02::keystore::encrypt_and_store_seed(&seed, "password").unwrap();
238+
bitbox02::memory::set_initialized().unwrap();
239+
240+
let mut password_entered: bool = false;
241+
242+
let mut mock_hal = TestingHal::new();
243+
mock_hal.sd.inserted = Some(true);
244+
mock_hal.ui.set_enter_string(Box::new(|_params| {
245+
password_entered = true;
246+
Ok("password".into())
247+
}));
248+
bitbox02::securechip::fake_event_counter_reset();
249+
assert_eq!(
250+
block_on(create(
251+
&mut mock_hal,
252+
&pb::CreateBackupRequest {
253+
timestamp: TIMESTMAP,
254+
timezone_offset: 18000,
255+
}
256+
)),
257+
Ok(Response::Success(pb::Success {}))
258+
);
259+
assert_eq!(bitbox02::securechip::fake_event_counter(), 5);
260+
assert_eq!(
261+
mock_hal.ui.screens,
262+
vec![
263+
Screen::Confirm {
264+
title: "Is today?".into(),
265+
body: "Mon 2020-09-28".into(),
266+
longtouch: false
267+
},
268+
Screen::Status {
269+
title: "Backup created".into(),
270+
success: true
271+
}
272+
]
273+
);
274+
275+
mock_hal.ui.remove_enter_string(); // no more password entry needed
276+
assert_eq!(
277+
block_on(check(
278+
&mut mock_hal,
279+
&pb::CheckBackupRequest { silent: true }
280+
)),
281+
Ok(Response::CheckBackup(pb::CheckBackupResponse {
282+
id: backup::id(&seed),
283+
}))
284+
);
285+
286+
drop(mock_hal); // to remove mutable borrow of `password_entered`
287+
assert!(password_entered);
288+
}
289+
226290
/// Use backup file fixtures generated using firmware v9.12.0 and perform tests on it. This
227291
/// should catch regressions when changing backup loading/verification in the firmware code.
228292
#[test]

src/rust/bitbox02-rust/src/hww/api/show_mnemonic.rs

Lines changed: 18 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -22,18 +22,20 @@ use pb::response::Response;
2222
use crate::hal::Ui;
2323
use crate::workflow::{confirm, unlock};
2424

25-
use crate::keystore;
26-
2725
/// Handle the ShowMnemonic API call. This shows the seed encoded as
2826
/// 12/18/24 BIP39 English words. Afterwards, for each word, the user
2927
/// is asked to pick the right word among 5 words, to check if they
3028
/// wrote it down correctly.
3129
pub async fn process(hal: &mut impl crate::hal::Hal) -> Result<Response, Error> {
32-
if bitbox02::memory::is_initialized() {
33-
unlock::unlock_keystore(hal, "Unlock device", unlock::CanCancel::Yes).await?;
34-
}
30+
let mnemonic_sentence = {
31+
let seed = if bitbox02::memory::is_initialized() {
32+
unlock::unlock_keystore(hal, "Unlock device", unlock::CanCancel::Yes).await?
33+
} else {
34+
bitbox02::keystore::copy_seed()?
35+
};
3536

36-
let mnemonic_sentence = keystore::get_bip39_mnemonic()?;
37+
bitbox02::keystore::bip39_mnemonic_from_seed(&seed)?
38+
};
3739

3840
hal.ui()
3941
.confirm(&confirm::Params {
@@ -139,17 +141,20 @@ mod tests {
139141

140142
bitbox02::memory::set_initialized().unwrap();
141143

144+
let mut password_entered: bool = false;
145+
142146
let mut mock_hal = TestingHal::new();
143-
mock_hal
144-
.ui
145-
.set_enter_string(Box::new(|_params| Ok("password".into())));
147+
mock_hal.ui.set_enter_string(Box::new(|_params| {
148+
password_entered = true;
149+
Ok("password".into())
150+
}));
146151

147152
bitbox02::securechip::fake_event_counter_reset();
148153
assert_eq!(
149154
block_on(process(&mut mock_hal)),
150155
Ok(Response::Success(pb::Success {}))
151156
);
152-
assert_eq!(bitbox02::securechip::fake_event_counter(), 6);
157+
assert_eq!(bitbox02::securechip::fake_event_counter(), 5);
153158

154159
assert_eq!(
155160
mock_hal.ui.screens,
@@ -173,6 +178,9 @@ mod tests {
173178
},
174179
]
175180
);
181+
182+
drop(mock_hal); // to remove mutable borrow of `password_entered`
183+
assert!(password_entered);
176184
}
177185

178186
/// When initialized, a password check is prompted before displaying the mnemonic.

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

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,4 +206,8 @@ impl<'a> TestingWorkflows<'a> {
206206
pub fn set_enter_string(&mut self, cb: EnterStringCb<'a>) {
207207
self._enter_string = Some(cb);
208208
}
209+
210+
pub fn remove_enter_string(&mut self) {
211+
self._enter_string = None;
212+
}
209213
}

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

Lines changed: 62 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ use bitbox02::keystore;
1919

2020
pub use password::CanCancel;
2121

22+
use alloc::vec::Vec;
23+
2224
/// Confirm the entered mnemonic passphrase with the user. Returns true if the user confirmed it,
2325
/// false if the user rejected it.
2426
async fn confirm_mnemonic_passphrase(
@@ -79,7 +81,7 @@ pub async fn unlock_keystore(
7981
hal: &mut impl crate::hal::Hal,
8082
title: &str,
8183
can_cancel: password::CanCancel,
82-
) -> Result<(), UnlockError> {
84+
) -> Result<zeroize::Zeroizing<Vec<u8>>, UnlockError> {
8385
let password = password::enter(
8486
hal,
8587
title,
@@ -89,7 +91,7 @@ pub async fn unlock_keystore(
8991
.await?;
9092

9193
match keystore::unlock(&password) {
92-
Ok(()) => Ok(()),
94+
Ok(seed) => Ok(seed),
9395
Err(keystore::Error::IncorrectPassword { remaining_attempts }) => {
9496
let msg = match remaining_attempts {
9597
1 => "Wrong password\n1 try remains".into(),
@@ -171,11 +173,63 @@ pub async fn unlock(hal: &mut impl crate::hal::Hal) -> Result<(), ()> {
171173
}
172174

173175
// Loop unlock until the password is correct or the device resets.
174-
while unlock_keystore(hal, "Enter password", password::CanCancel::No)
175-
.await
176-
.is_err()
177-
{}
176+
loop {
177+
if let Ok(seed) = unlock_keystore(hal, "Enter password", password::CanCancel::No).await {
178+
unlock_bip39(hal, &seed).await;
179+
return Ok(());
180+
}
181+
}
182+
}
178183

179-
unlock_bip39(hal, &bitbox02::keystore::copy_seed()?).await;
180-
Ok(())
184+
#[cfg(test)]
185+
mod tests {
186+
use super::*;
187+
188+
use crate::hal::testing::TestingHal;
189+
use crate::workflow::testing::Screen;
190+
use alloc::boxed::Box;
191+
use bitbox02::testing::{mock_memory, mock_unlocked, mock_unlocked_using_mnemonic};
192+
use util::bb02_async::block_on;
193+
194+
#[test]
195+
fn test_unlock_success() {
196+
mock_memory();
197+
198+
// Set up an initialized wallet with password
199+
bitbox02::keystore::encrypt_and_store_seed(
200+
hex::decode("c7940c13479b8d9a6498f4e50d5a42e0d617bc8e8ac9f2b8cecf97e94c2b035c")
201+
.unwrap()
202+
.as_slice(),
203+
"password",
204+
)
205+
.unwrap();
206+
207+
bitbox02::memory::set_initialized().unwrap();
208+
209+
// Lock the keystore to simulate the normal locked state
210+
bitbox02::keystore::lock();
211+
212+
let mut password_entered = false;
213+
214+
let mut mock_hal = TestingHal::new();
215+
mock_hal.ui.set_enter_string(Box::new(|_params| {
216+
password_entered = true;
217+
Ok("password".into())
218+
}));
219+
bitbox02::securechip::fake_event_counter_reset();
220+
assert_eq!(block_on(unlock(&mut mock_hal)), Ok(()));
221+
// 6 for keystore unlock, 1 for keystore bip39 unlock.
222+
assert_eq!(bitbox02::securechip::fake_event_counter(), 7);
223+
224+
assert!(!bitbox02::keystore::is_locked());
225+
226+
assert_eq!(
227+
bitbox02::keystore::copy_bip39_seed().unwrap().as_slice(),
228+
hex::decode("cff4b263e5b0eb299e5fd35fcd09988f6b14e5b464f8d18fb84b152f889dd2a30550f4c2b346cae825ffedd4a87fc63fc12a9433de5125b6c7fdbc5eab0c590b")
229+
.unwrap(),
230+
);
231+
232+
drop(mock_hal); // to remove mutable borrow of `password_entered`
233+
assert!(password_entered);
234+
}
181235
}

src/rust/bitbox02/src/keystore.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,9 +67,11 @@ impl core::convert::From<keystore_error_t> for Error {
6767
}
6868
}
6969

70-
pub fn unlock(password: &str) -> Result<(), Error> {
70+
pub fn unlock(password: &str) -> Result<zeroize::Zeroizing<Vec<u8>>, Error> {
7171
let mut remaining_attempts: u8 = 0;
7272
let mut securechip_result: i32 = 0;
73+
let mut seed = zeroize::Zeroizing::new([0u8; MAX_SEED_LENGTH].to_vec());
74+
let mut seed_len: usize = 0;
7375
match unsafe {
7476
bitbox02_sys::keystore_unlock(
7577
crate::util::str_to_cstr_vec(password)
@@ -78,9 +80,14 @@ pub fn unlock(password: &str) -> Result<(), Error> {
7880
.cast(),
7981
&mut remaining_attempts,
8082
&mut securechip_result,
83+
seed.as_mut_ptr(),
84+
&mut seed_len,
8185
)
8286
} {
83-
keystore_error_t::KEYSTORE_OK => Ok(()),
87+
keystore_error_t::KEYSTORE_OK => {
88+
seed.truncate(seed_len);
89+
Ok(seed)
90+
}
8491
keystore_error_t::KEYSTORE_ERR_INCORRECT_PASSWORD => {
8592
Err(Error::IncorrectPassword { remaining_attempts })
8693
}
@@ -483,15 +490,15 @@ mod tests {
483490

484491
// First call: unlock. The first one does a seed rentention (1 securechip event).
485492
crate::securechip::fake_event_counter_reset();
486-
assert!(unlock("password").is_ok());
493+
assert_eq!(unlock("password").unwrap().as_slice(), seed);
487494
assert_eq!(crate::securechip::fake_event_counter(), 6);
488495

489496
// Loop to check that unlocking works while unlocked.
490497
for _ in 0..2 {
491498
// Further calls perform a password check.The password check does not do the retention
492499
// so it ends up needing one secure chip operation less.
493500
crate::securechip::fake_event_counter_reset();
494-
assert!(unlock("password").is_ok());
501+
assert_eq!(unlock("password").unwrap().as_slice(), seed);
495502
assert_eq!(crate::securechip::fake_event_counter(), 5);
496503
}
497504

@@ -630,7 +637,9 @@ mod tests {
630637
.is_err()
631638
);
632639
// Correct seed passed.
640+
crate::securechip::fake_event_counter_reset();
633641
assert!(block_on(unlock_bip39(&secp, &seed, "foo", async || {})).is_ok());
642+
assert_eq!(crate::securechip::fake_event_counter(), 1);
634643
assert_eq!(root_fingerprint(), Ok(vec![0xf1, 0xbc, 0x3c, 0x46]),);
635644

636645
let expected_bip39_seed = hex::decode("2b3c63de86f0f2b13cc6a36c1ba2314fbc1b40c77ab9cb64e96ba4d5c62fc204748ca6626a9f035e7d431bce8c9210ec0bdffc2e7db873dee56c8ac2153eee9a").unwrap();
@@ -769,7 +778,7 @@ mod tests {
769778

770779
// Correct password. First time: unlock. After unlock, it becomes a password check.
771780
for _ in 0..3 {
772-
assert!(unlock("foo").is_ok());
781+
assert_eq!(unlock("foo").unwrap().as_slice(), &seed[..seed_size]);
773782
}
774783
assert_eq!(copy_seed().unwrap().as_slice(), &seed[..seed_size]);
775784

0 commit comments

Comments
 (0)