Skip to content

Commit 829ffa0

Browse files
committed
keystore: port bip85_ln() to Rust
The sha512 digest adaptor is moved to a util file as it's no longer only used by ed25519.
1 parent 81b8f45 commit 829ffa0

File tree

9 files changed

+123
-112
lines changed

9 files changed

+123
-112
lines changed

src/keystore.c

Lines changed: 0 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -793,30 +793,6 @@ bool keystore_bip85_bip39(
793793
return snprintf_result >= 0 && snprintf_result < (int)mnemonic_out_size;
794794
}
795795

796-
bool keystore_bip85_ln(uint32_t index, uint8_t* entropy_out)
797-
{
798-
if (index >= BIP32_INITIAL_HARDENED_CHILD) {
799-
return false;
800-
}
801-
802-
const uint32_t keypath[] = {
803-
83696968 + BIP32_INITIAL_HARDENED_CHILD,
804-
19534 + BIP32_INITIAL_HARDENED_CHILD,
805-
0 + BIP32_INITIAL_HARDENED_CHILD,
806-
12 + BIP32_INITIAL_HARDENED_CHILD,
807-
index + BIP32_INITIAL_HARDENED_CHILD,
808-
};
809-
810-
uint8_t entropy[64] = {0};
811-
UTIL_CLEANUP_64(entropy);
812-
if (!_bip85_entropy(keypath, sizeof(keypath) / sizeof(uint32_t), entropy)) {
813-
return false;
814-
}
815-
816-
memcpy(entropy_out, entropy, 16);
817-
return true;
818-
}
819-
820796
USE_RESULT bool keystore_encode_xpub_at_keypath(
821797
const uint32_t* keypath,
822798
size_t keypath_len,

src/keystore.h

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -245,16 +245,6 @@ USE_RESULT bool keystore_bip85_bip39(
245245
char* mnemonic_out,
246246
size_t mnemonic_out_size);
247247

248-
/**
249-
* Computes a 16 byte deterministic seed specifically for Lightning hot wallets according to BIP-85.
250-
* It is the same as BIP-85 with app number 39', but instead using app number 19534' (= 0x4c4e =
251-
* 'LN'). https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki#bip39
252-
* Restricted to 16 byte output entropy.
253-
* @param[in] index must be smaller than `BIP32_INITIAL_HARDENED_CHILD`.
254-
* @param[out] entropy_out resulting entropy, must be at least 16 bytes in size.
255-
*/
256-
USE_RESULT bool keystore_bip85_ln(uint32_t index, uint8_t* entropy_out);
257-
258248
/**
259249
* Encode an xpub at the given `keypath` as 78 bytes according to BIP32. The version bytes are
260250
* the ones corresponding to `xpub`, i.e. 0x0488B21E.

src/rust/bitbox02-rust/src/hash.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
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 alloc::vec::Vec;
16+
17+
/// Implements the digest traits for Sha512 backing it with the wally_sha512 C function. This is
18+
/// done to avoid using a second sha512 implementation like `sha2::Sha512`, which bloats the binary
19+
/// by an additional ~12.7kB (at the time of writing).
20+
///
21+
/// This implementation accumulates the data to be hashed in heap, it does **not** hash in a
22+
/// streaming fashion, even when using `update()`.
23+
#[derive(Default, Clone)]
24+
pub struct Sha512(Vec<u8>);
25+
26+
impl digest::HashMarker for Sha512 {}
27+
28+
impl digest::OutputSizeUser for Sha512 {
29+
type OutputSize = digest::typenum::U64;
30+
}
31+
32+
impl digest::FixedOutput for Sha512 {
33+
fn finalize_into(self, out: &mut digest::Output<Self>) {
34+
// use digest::Digest;
35+
// out.copy_from_slice(&sha2::Sha512::digest(&self.0));
36+
out.copy_from_slice(&bitbox02::sha512(&self.0));
37+
}
38+
}
39+
40+
impl digest::Update for Sha512 {
41+
fn update(&mut self, data: &[u8]) {
42+
self.0.extend(data);
43+
}
44+
}
45+
46+
impl digest::Reset for Sha512 {
47+
fn reset(&mut self) {
48+
self.0 = vec![];
49+
}
50+
}
51+
52+
impl digest::core_api::BlockSizeUser for Sha512 {
53+
type BlockSize = digest::typenum::U128;
54+
}

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -160,5 +160,7 @@ async fn process_ln(
160160
})
161161
.await?;
162162

163-
keystore::bip85_ln(account_number).map_err(|_| Error::Generic)
163+
Ok(crate::keystore::bip85_ln(account_number)
164+
.map_err(|_| Error::Generic)?
165+
.to_vec())
164166
}

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

Lines changed: 64 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ use alloc::vec::Vec;
2020
use crate::bip32;
2121
use bitbox02::keystore;
2222

23+
use util::bip32::HARDENED;
24+
25+
use crate::hash::Sha512;
26+
use hmac::{digest::FixedOutput, Mac, SimpleHmac};
27+
2328
/// Derives an xpub from the keystore seed at the given keypath.
2429
pub fn get_xpub(keypath: &[u32]) -> Result<bip32::Xpub, ()> {
2530
// Convert from C keystore to Rust by encoding the xpub in C and decoding it in Rust.
@@ -33,12 +38,43 @@ pub fn root_fingerprint() -> Result<Vec<u8>, ()> {
3338
Ok(get_xpub(&[])?.pubkey_hash160().get(..4).ok_or(())?.to_vec())
3439
}
3540

41+
fn bip85_entropy(keypath: &[u32]) -> Result<zeroize::Zeroizing<Vec<u8>>, ()> {
42+
let priv_key = keystore::secp256k1_get_private_key(keypath)?;
43+
let mut mac = SimpleHmac::<Sha512>::new_from_slice(b"bip-entropy-from-k").unwrap();
44+
mac.update(&priv_key);
45+
let mut out = zeroize::Zeroizing::new(vec![0u8; 64]);
46+
let fixed_out: &mut [u8; 64] = out.as_mut_slice().try_into().unwrap();
47+
mac.finalize_into(fixed_out.into());
48+
Ok(out)
49+
}
50+
51+
/// Computes a 16 byte deterministic seed specifically for Lightning hot wallets according to BIP-85.
52+
/// It is the same as BIP-85 with app number 39', but instead using app number 19534' (= 0x4c4e =
53+
/// 'LN'). https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki#bip39
54+
/// Restricted to 16 byte output entropy.
55+
/// `index` must be smaller than `bip32::HARDENED`.
56+
pub fn bip85_ln(index: u32) -> Result<zeroize::Zeroizing<Vec<u8>>, ()> {
57+
if index >= HARDENED {
58+
return Err(());
59+
}
60+
let keypath = [
61+
83696968 + HARDENED,
62+
19534 + HARDENED,
63+
0 + HARDENED,
64+
12 + HARDENED,
65+
index + HARDENED,
66+
];
67+
68+
let mut entropy = bip85_entropy(&keypath)?;
69+
entropy.truncate(16);
70+
Ok(entropy)
71+
}
72+
3673
#[cfg(test)]
3774
mod tests {
3875
use super::*;
3976

4077
use bitbox02::testing::{mock_unlocked, mock_unlocked_using_mnemonic};
41-
use util::bip32::HARDENED;
4278

4379
#[test]
4480
fn test_get_xpub() {
@@ -99,4 +135,31 @@ mod tests {
99135
);
100136
assert_eq!(root_fingerprint(), Ok(vec![0xf4, 0x0b, 0x46, 0x9a]));
101137
}
138+
139+
#[test]
140+
fn test_bip85_ln() {
141+
keystore::lock();
142+
assert!(bip85_ln(0).is_err());
143+
144+
mock_unlocked_using_mnemonic(
145+
"virtual weapon code laptop defy cricket vicious target wave leopard garden give",
146+
"",
147+
);
148+
149+
assert_eq!(
150+
bip85_ln(0).unwrap().as_slice(),
151+
b"\x3a\x5f\x3b\x88\x8a\xab\x88\xe2\xa9\xab\x99\x1b\x60\xa0\x3e\xd8",
152+
);
153+
assert_eq!(
154+
bip85_ln(1).unwrap().as_slice(),
155+
b"\xe7\xd9\xce\x75\xf8\xcb\x17\x57\x0e\x66\x54\x17\xb4\x7f\xa0\xbe",
156+
);
157+
assert_eq!(
158+
bip85_ln(HARDENED - 1).unwrap().as_slice(),
159+
b"\x1f\x3b\x75\xea\x25\x27\x49\x70\x0a\x1e\x45\x34\x69\x14\x8c\xa6",
160+
);
161+
162+
// Index too high.
163+
assert!(bip85_ln(HARDENED).is_err());
164+
}
102165
}

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

Lines changed: 1 addition & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -14,48 +14,9 @@
1414

1515
use alloc::vec::Vec;
1616

17+
use crate::hash::Sha512;
1718
use bip32_ed25519::{Xprv, Xpub, ED25519_EXPANDED_SECRET_KEY_SIZE};
1819

19-
/// Implements the digest traits for Sha512 backing it with the wally_sha512 C function. This is
20-
/// done to avoid using a second sha512 implementation like `sha2::Sha512`, which bloats the binary
21-
/// by an additional ~12.7kB (at the time of writing).
22-
///
23-
/// This implementation accumulates the data to be hashed in heap, it does **not** hash in a
24-
/// streaming fashion, even when using `update()`. This is okay for the use within this module, as
25-
/// bip32_ed25519 and sign_raw() do not hash a lot of data.
26-
#[derive(Default, Clone)]
27-
pub struct Sha512(Vec<u8>);
28-
29-
impl digest::HashMarker for Sha512 {}
30-
31-
impl digest::OutputSizeUser for Sha512 {
32-
type OutputSize = digest::typenum::U64;
33-
}
34-
35-
impl digest::FixedOutput for Sha512 {
36-
fn finalize_into(self, out: &mut digest::Output<Self>) {
37-
// use digest::Digest;
38-
// out.copy_from_slice(&sha2::Sha512::digest(&self.0));
39-
out.copy_from_slice(&bitbox02::sha512(&self.0));
40-
}
41-
}
42-
43-
impl digest::Update for Sha512 {
44-
fn update(&mut self, data: &[u8]) {
45-
self.0.extend(data);
46-
}
47-
}
48-
49-
impl digest::Reset for Sha512 {
50-
fn reset(&mut self) {
51-
self.0 = vec![];
52-
}
53-
}
54-
55-
impl digest::core_api::BlockSizeUser for Sha512 {
56-
type BlockSize = digest::typenum::U128;
57-
}
58-
5920
fn get_seed() -> Result<zeroize::Zeroizing<Vec<u8>>, ()> {
6021
bitbox02::keystore::get_ed25519_seed()
6122
}

src/rust/bitbox02-rust/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ pub mod backup;
3232
pub mod bb02_async;
3333
mod bip32;
3434
pub mod hal;
35+
mod hash;
3536
pub mod hww;
3637
pub mod keystore;
3738
mod version;

src/rust/bitbox02-sys/build.rs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,6 @@ const ALLOWLIST_FNS: &[&str] = &[
6868
"empty_create",
6969
"keystore_bip39_mnemonic_to_seed",
7070
"keystore_bip85_bip39",
71-
"keystore_bip85_ln",
7271
"keystore_copy_seed",
7372
"keystore_create_and_store_seed",
7473
"keystore_encode_xpub_at_keypath",

src/rust/bitbox02/src/keystore.rs

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -328,14 +328,6 @@ pub fn bip85_bip39(words: u32, index: u32) -> Result<zeroize::Zeroizing<String>,
328328
}
329329
}
330330

331-
pub fn bip85_ln(index: u32) -> Result<Vec<u8>, ()> {
332-
let mut entropy = vec![0u8; 16];
333-
match unsafe { bitbox02_sys::keystore_bip85_ln(index, entropy.as_mut_ptr()) } {
334-
false => Err(()),
335-
true => Ok(entropy),
336-
}
337-
}
338-
339331
pub fn secp256k1_schnorr_sign(
340332
keypath: &[u32],
341333
msg: &[u8; 32],
@@ -512,33 +504,6 @@ mod tests {
512504
assert!(bip85_bip39(12, HARDENED).is_err());
513505
}
514506

515-
#[test]
516-
fn test_bip85_ln() {
517-
lock();
518-
assert!(bip85_ln(0).is_err());
519-
520-
mock_unlocked_using_mnemonic(
521-
"virtual weapon code laptop defy cricket vicious target wave leopard garden give",
522-
"",
523-
);
524-
525-
assert_eq!(
526-
bip85_ln(0).unwrap().as_slice(),
527-
b"\x3a\x5f\x3b\x88\x8a\xab\x88\xe2\xa9\xab\x99\x1b\x60\xa0\x3e\xd8",
528-
);
529-
assert_eq!(
530-
bip85_ln(1).unwrap().as_slice(),
531-
b"\xe7\xd9\xce\x75\xf8\xcb\x17\x57\x0e\x66\x54\x17\xb4\x7f\xa0\xbe",
532-
);
533-
assert_eq!(
534-
bip85_ln(HARDENED - 1).unwrap().as_slice(),
535-
b"\x1f\x3b\x75\xea\x25\x27\x49\x70\x0a\x1e\x45\x34\x69\x14\x8c\xa6",
536-
);
537-
538-
// Index too high.
539-
assert!(bip85_ln(HARDENED).is_err());
540-
}
541-
542507
#[test]
543508
fn test_secp256k1_get_private_key() {
544509
lock();

0 commit comments

Comments
 (0)