Skip to content

Commit 278da3a

Browse files
committed
Merge branch 'cedwies/unlock-attempts' into HEAD
2 parents 647cec6 + 1266caf commit 278da3a

File tree

6 files changed

+67
-42
lines changed

6 files changed

+67
-42
lines changed

src/memory/bitbox02_smarteeprom.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,9 @@
2020
/**
2121
* After this many failed unlock attempts, the keystore becomes locked until a
2222
* device reset.
23+
*
24+
* Must match MAX_UNLOCK_ATTEMPTS in rust keystore.rs and
25+
* SMALL_MONOTONIC_COUNTER_MAX_USE in optiga.c.
2326
*/
2427
#define MAX_UNLOCK_ATTEMPTS (10)
2528

src/optiga/optiga.c

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,12 @@
4949
// indication of 600000 updates. See Solution Reference Manual Figure 32.
5050
#define MONOTONIC_COUNTER_MAX_USE (590000)
5151

52+
// Number of times the password can be entered incorrectly before further password stretching fails.
53+
// The counter is reset when the correct password is entered.
54+
// Must match MAX_UNLOCK_ATTEMPTS in bitbox02_smarteeprom.h and
55+
// MAX_UNLOCK_ATTEMPTS in rust keystore.rs.
56+
#define SMALL_MONOTONIC_COUNTER_MAX_USE (MAX_UNLOCK_ATTEMPTS)
57+
5258
// Maximum size of metadata. See "Metadata Update Identifier":
5359
// https://github.com/Infineon/optiga-trust-m-overview/blob/98b2b9c178f0391b1ab26b52082899704dab688a/docs/OPTIGA%E2%84%A2%20Trust%20M%20Solution%20Reference%20Manual.md#linka946a953_def2_41cf_850a_74fb7899fe11
5460
// Two extra bytes for the `0x20 <len>` header bytes.

src/rust/bitbox02-rust/src/hal.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ pub trait Memory {
7575
fn get_encrypted_seed_and_hmac(&mut self) -> Result<alloc::vec::Vec<u8>, ()>;
7676
fn set_encrypted_seed_and_hmac(&mut self, data: &[u8]) -> Result<(), ()>;
7777
fn reset_hww(&mut self) -> Result<(), ()>;
78+
fn get_unlock_attempts(&mut self) -> u8;
79+
fn increment_unlock_attempts(&mut self);
80+
fn reset_unlock_attempts(&mut self);
7881
}
7982

8083
/// Hardware abstraction layer for BitBox devices.
@@ -243,6 +246,16 @@ impl Memory for BitBox02Memory {
243246
fn reset_hww(&mut self) -> Result<(), ()> {
244247
bitbox02::memory::reset_hww()
245248
}
249+
250+
fn get_unlock_attempts(&mut self) -> u8 {
251+
bitbox02::memory::smarteeprom_get_unlock_attempts()
252+
}
253+
fn increment_unlock_attempts(&mut self) {
254+
bitbox02::memory::smarteeprom_increment_unlock_attempts()
255+
}
256+
fn reset_unlock_attempts(&mut self) {
257+
bitbox02::memory::smarteeprom_reset_unlock_attempts()
258+
}
246259
}
247260

248261
pub struct BitBox02Hal {
@@ -403,6 +416,7 @@ pub mod testing {
403416
seed_birthdate: u32,
404417
encrypted_seed_and_hmac: Option<Vec<u8>>,
405418
device_name: Option<String>,
419+
unlock_attempts: u8,
406420
}
407421

408422
impl TestingSecureChip {
@@ -522,6 +536,7 @@ pub mod testing {
522536
seed_birthdate: 0,
523537
encrypted_seed_and_hmac: None,
524538
device_name: None,
539+
unlock_attempts: 0,
525540
}
526541
}
527542

@@ -532,6 +547,10 @@ pub mod testing {
532547
pub fn set_platform(&mut self, platform: bitbox02::memory::Platform) {
533548
self.platform = platform;
534549
}
550+
551+
pub fn set_unlock_attempts_for_testing(&mut self, attempts: u8) {
552+
self.unlock_attempts = attempts;
553+
}
535554
}
536555

537556
impl super::Memory for TestingMemory {
@@ -608,6 +627,18 @@ pub mod testing {
608627
self.device_name = None;
609628
Ok(())
610629
}
630+
631+
fn get_unlock_attempts(&mut self) -> u8 {
632+
self.unlock_attempts
633+
}
634+
635+
fn increment_unlock_attempts(&mut self) {
636+
self.unlock_attempts += 1;
637+
}
638+
639+
fn reset_unlock_attempts(&mut self) {
640+
self.unlock_attempts = 0;
641+
}
611642
}
612643

613644
pub struct TestingHal<'a> {

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

Lines changed: 26 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,10 @@ const ENCRYPTION_OVERHEAD: usize = 64;
4040
// to roughly seven seconds so we don't assume communication was lost mid-unlock.
4141
const LONG_TIMEOUT: i16 = -70;
4242

43+
// Must match MAX_UNLOCK_ATTEMPTS in bitbox02_smarteeprom.h and
44+
// SMALL_MONOTONIC_COUNTER_MAX_USE in optiga.c.
45+
const MAX_UNLOCK_ATTEMPTS: u8 = 10;
46+
4347
#[derive(Debug)]
4448
pub enum Error {
4549
InvalidState,
@@ -337,7 +341,7 @@ pub async fn unlock(
337341
if !hal.memory().is_seeded() {
338342
return Err(Error::Unseeded);
339343
}
340-
if get_remaining_unlock_attempts() == 0 {
344+
if get_remaining_unlock_attempts(hal) == 0 {
341345
//
342346
// We reset the device as soon as the MAX_UNLOCK_ATTEMPTSth attempt
343347
// is made. So we should never enter this branch...
@@ -347,11 +351,11 @@ pub async fn unlock(
347351
return Err(Error::MaxAttemptsExceeded);
348352
}
349353
bitbox02::usb_processing::timeout_reset(LONG_TIMEOUT);
350-
bitbox02::memory::smarteeprom_increment_unlock_attempts();
354+
hal.memory().increment_unlock_attempts();
351355
let seed = match get_and_decrypt_seed(hal, password) {
352356
Ok(seed) => seed,
353357
err @ Err(_) => {
354-
if get_remaining_unlock_attempts() == 0 {
358+
if get_remaining_unlock_attempts(hal) == 0 {
355359
crate::reset::reset(hal, false).await;
356360
return Err(Error::MaxAttemptsExceeded);
357361
}
@@ -367,15 +371,15 @@ pub async fn unlock(
367371
} else {
368372
retain_seed(hal, &seed)?;
369373
}
370-
bitbox02::memory::smarteeprom_reset_unlock_attempts();
374+
hal.memory().reset_unlock_attempts();
371375
Ok(seed)
372376
}
373377

374378
/// Returns the number of remaining unlock attempts (calls to `unlock()`) that are allowed before
375379
/// the device resets itself.
376-
pub fn get_remaining_unlock_attempts() -> u8 {
377-
let failed_attempts: u8 = bitbox02::memory::smarteeprom_get_unlock_attempts();
378-
bitbox02::memory::MAX_UNLOCK_ATTEMPTS.saturating_sub(failed_attempts)
380+
pub fn get_remaining_unlock_attempts(hal: &mut impl crate::hal::Hal) -> u8 {
381+
let failed_attempts: u8 = hal.memory().get_unlock_attempts();
382+
MAX_UNLOCK_ATTEMPTS.saturating_sub(failed_attempts)
379383
}
380384

381385
/// Unlocks the bip39 seed. The input seed must be the keystore seed (i.e. must match the output
@@ -1173,14 +1177,14 @@ mod tests {
11731177
assert_eq!(decrypted.as_slice(), seed.as_slice());
11741178

11751179
// First 9 wrong attempts.
1176-
for i in 1..bitbox02::memory::MAX_UNLOCK_ATTEMPTS {
1180+
for i in 1..MAX_UNLOCK_ATTEMPTS {
11771181
assert!(matches!(
11781182
block_on(unlock(&mut mock_hal, "invalid password")),
11791183
Err(Error::IncorrectPassword)
11801184
));
11811185
assert_eq!(
1182-
get_remaining_unlock_attempts(),
1183-
bitbox02::memory::MAX_UNLOCK_ATTEMPTS - i
1186+
get_remaining_unlock_attempts(&mut mock_hal),
1187+
MAX_UNLOCK_ATTEMPTS - i
11841188
);
11851189
// Still seeded.
11861190
assert!(mock_hal.memory.is_seeded());
@@ -1218,15 +1222,15 @@ mod tests {
12181222
assert!(is_locked());
12191223
assert!(copy_seed(&mut mock_hal).is_err());
12201224

1221-
for attempt in 1..bitbox02::memory::MAX_UNLOCK_ATTEMPTS {
1225+
for attempt in 1..MAX_UNLOCK_ATTEMPTS {
12221226
assert!(matches!(
12231227
block_on(unlock(&mut mock_hal, "invalid password")),
12241228
Err(Error::IncorrectPassword),
12251229
));
12261230

12271231
assert_eq!(
1228-
get_remaining_unlock_attempts(),
1229-
bitbox02::memory::MAX_UNLOCK_ATTEMPTS - attempt
1232+
get_remaining_unlock_attempts(&mut mock_hal),
1233+
MAX_UNLOCK_ATTEMPTS - attempt
12301234
);
12311235
assert!(is_locked());
12321236
assert!(copy_seed(&mut mock_hal).is_err());
@@ -1265,12 +1269,12 @@ mod tests {
12651269
lock();
12661270
assert!(is_locked());
12671271

1268-
bitbox02::memory::set_unlock_attempts_for_testing(bitbox02::memory::MAX_UNLOCK_ATTEMPTS);
1269-
assert_eq!(get_remaining_unlock_attempts(), 0);
1270-
assert_eq!(
1271-
bitbox02::memory::smarteeprom_get_unlock_attempts(),
1272-
bitbox02::memory::MAX_UNLOCK_ATTEMPTS
1273-
);
1272+
mock_hal
1273+
.memory
1274+
.set_unlock_attempts_for_testing(MAX_UNLOCK_ATTEMPTS);
1275+
1276+
assert_eq!(get_remaining_unlock_attempts(&mut mock_hal), 0);
1277+
assert_eq!(mock_hal.memory.get_unlock_attempts(), MAX_UNLOCK_ATTEMPTS);
12741278

12751279
assert!(matches!(
12761280
block_on(unlock(&mut mock_hal, "password")),
@@ -1303,10 +1307,7 @@ mod tests {
13031307
block_on(unlock(hal, "wrong")),
13041308
Err(Error::IncorrectPassword)
13051309
));
1306-
assert_eq!(
1307-
get_remaining_unlock_attempts(),
1308-
bitbox02::memory::MAX_UNLOCK_ATTEMPTS - 1
1309-
);
1310+
assert_eq!(get_remaining_unlock_attempts(hal), MAX_UNLOCK_ATTEMPTS - 1);
13101311
}
13111312

13121313
wrong_attempt(&mut mock_hal);
@@ -1358,10 +1359,7 @@ mod tests {
13581359
block_on(unlock(hal, "wrong")),
13591360
Err(Error::IncorrectPassword)
13601361
));
1361-
assert_eq!(
1362-
get_remaining_unlock_attempts(),
1363-
bitbox02::memory::MAX_UNLOCK_ATTEMPTS - 1
1364-
);
1362+
assert_eq!(get_remaining_unlock_attempts(hal), MAX_UNLOCK_ATTEMPTS - 1);
13651363
}
13661364

13671365
wrong_attempt(&mut mock_hal);
@@ -2049,7 +2047,7 @@ mod tests {
20492047
block_on(unlock(&mut mock_hal, "bar")),
20502048
Err(Error::IncorrectPassword)
20512049
));
2052-
assert_eq!(get_remaining_unlock_attempts(), 9);
2050+
assert_eq!(get_remaining_unlock_attempts(&mut mock_hal), 9);
20532051

20542052
// Correct password. First time: unlock. After unlock, it becomes a password check.
20552053
for _ in 0..3 {

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,7 @@ pub async fn unlock_keystore(
9292
match crate::keystore::unlock(hal, &password).await {
9393
Ok(seed) => Ok(seed),
9494
Err(crate::keystore::Error::IncorrectPassword) => {
95-
let msg = match crate::keystore::get_remaining_unlock_attempts() {
95+
let msg = match crate::keystore::get_remaining_unlock_attempts(hal) {
9696
1 => "Wrong password\n1 try remains".into(),
9797
n => format!("Wrong password\n{} tries remain", n),
9898
};

src/rust/bitbox02/src/memory.rs

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,6 @@ extern crate alloc;
1717
use alloc::string::String;
1818
use alloc::vec::Vec;
1919

20-
pub const MAX_UNLOCK_ATTEMPTS: u8 = bitbox02_sys::MAX_UNLOCK_ATTEMPTS as u8;
21-
2220
// deduct one for the null terminator.
2321
pub const DEVICE_NAME_MAX_LEN: usize = bitbox02_sys::MEMORY_DEVICE_NAME_MAX_LEN as usize - 1;
2422

@@ -186,17 +184,6 @@ pub fn smarteeprom_reset_unlock_attempts() {
186184
}
187185
}
188186

189-
/// Testing helper to set the recorded number of unlock attempts to a precise value.
190-
/// Panics if `attempts` exceeds the maximum supported by the firmware.
191-
#[cfg(feature = "testing")]
192-
pub fn set_unlock_attempts_for_testing(attempts: u8) {
193-
assert!(attempts <= MAX_UNLOCK_ATTEMPTS);
194-
smarteeprom_reset_unlock_attempts();
195-
for _ in 0..attempts {
196-
smarteeprom_increment_unlock_attempts();
197-
}
198-
}
199-
200187
pub fn multisig_set_by_hash(hash: &[u8], name: &str) -> Result<(), MemoryError> {
201188
if hash.len() != 32 {
202189
return Err(MemoryError::MEMORY_ERR_INVALID_INPUT);

0 commit comments

Comments
 (0)