Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(wallet): smarter key exports #2386

Open
wants to merge 7 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion crypto/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ memzero = "0.1.0"
rand = "0.7.3"
ring = "0.16.11"
secp256k1 = { version = "0.22.2", features = ["global-context"] }
serde = { version = "1.0.104", optional = true }
serde = { version = "1.0.104", optional = true, features=["derive"] }
sha2 = "0.8.1"
tiny-bip39 = "0.7.0"

Expand Down
115 changes: 92 additions & 23 deletions crypto/src/key.rs
Original file line number Diff line number Diff line change
Expand Up @@ -180,7 +180,7 @@ impl ExtendedSK {
}

/// Create a new extended secret key from the given slip32-encoded string.
pub fn from_slip32(slip32: &str) -> Result<(Self, KeyPath), KeyError> {
pub fn from_slip32(slip32: &str) -> Result<(Self, KeyPath, Option<Vec<u8>>), KeyError> {
let (hrp, data) = bech32::decode(slip32).map_err(KeyError::deserialization_err)?;

if hrp.as_str() != "xprv" {
Expand All @@ -195,12 +195,12 @@ impl ExtendedSK {
let mut cursor = io::Cursor::new(bytes);
let depth = cursor.read_u8()? as usize;
let len = depth * 4;
let expected_len = len + 66; // 66 = 1 (depth) 32 (chain code) + 33 (private key)
let base_len = len + 66; // 66 = 1 (depth) 32 (chain code) + 33 (private key)

if expected_len != actual_len {
if actual_len < base_len {
return Err(KeyError::Deserialization(failure::format_err!(
"invalid data length, expected: {}, got: {}",
expected_len,
"invalid data length, expected at least {} bytes, got {} bytes",
base_len,
actual_len
)));
}
Expand All @@ -217,39 +217,57 @@ impl ExtendedSK {
let mut secret = Protected::new(vec![0; 32]);
cursor.read_exact(secret.as_mut())?;

// If there are bytes yet to read, put them into `extra_data`
let extra_len = actual_len - base_len;
let extra_data = if extra_len > 0 {
let mut extra_data = vec![0; extra_len];
cursor.read_exact(extra_data.as_mut())?;

Some(extra_data)
} else {
None
};

let sk = SK::from_slice(secret.as_ref())?;
let extended_sk = Self::new(sk, chain_code);

Ok((extended_sk, path.into()))
Ok((extended_sk, path.into(), extra_data))
}

/// Serialize the key following the SLIP32 spec.
///
/// See https://github.com/satoshilabs/slips/blob/master/slip-0032.md#serialization-format
pub fn to_slip32(&self, path: &KeyPath) -> Result<String, KeyError> {
pub fn to_slip32(
&self,
path: &KeyPath,
extra_data: Option<Vec<u8>>,
) -> Result<String, KeyError> {
let depth = path.depth();
let extra_data = extra_data.unwrap_or_default();

let capacity = 1 // 1 byte for depth
+ 4 * depth // 4 * depth bytes for path
+ 32 // 32 bytes for chain code
+ 33 // 33 bytes for 0x00 || private key
+ extra_data.len(); // any arbitrary amount of extra bytes
let mut bytes = Protected::new(vec![0; capacity]);
let mut slice = bytes.as_mut();

let depth = u8::try_from(depth).map_err(|_| {
KeyError::Serialization(failure::format_err!(
"path depth '{}' is greater than 255",
depth,
))
})?;

let capacity = 1 // 1 byte for depth
+ 4 * depth // 4 * depth bytes for path
+ 32 // 32 bytes for chain code
+ 33 // 33 bytes for 0x00 || private key
;
let mut bytes = Protected::new(vec![0; usize::from(capacity)]);
let mut slice = bytes.as_mut();

slice.write_all(&[depth])?;
for index in path.iter() {
slice.write_all(&index.as_ref().to_be_bytes())?;
}
slice.write_all(self.chain_code.as_ref())?;
slice.write_all(&[0])?;
slice.write_all(self.secret().as_ref())?;
slice.write_all(extra_data.as_ref())?;

let encoded = bech32::encode("xprv", bytes.as_ref().to_base32())
.map_err(KeyError::serialization_err)?;
Expand Down Expand Up @@ -321,7 +339,6 @@ impl From<ExtendedSK> for SK {
///
/// It can be used to derive other HD-Wallets public keys.
#[derive(Clone, PartialEq, Eq, Debug)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct ExtendedPK {
/// Public key
pub key: PK,
Expand Down Expand Up @@ -470,6 +487,40 @@ fn get_chain_code_and_secret(
Ok((chain_code, secret_key))
}

/// Exports a master key into SLIP-32 format with 2 extra `u32` fields inside the SLIP-32 data
/// holding the "next index" for the external and internal keychains.
pub fn slip32_from_master_and_indexes(
master_key: ExtendedSK,
external_index: u32,
internal_index: u32,
) -> Result<String, KeyError> {
let external_index = external_index.to_be_bytes().to_vec();
let internal_index = internal_index.to_be_bytes().to_vec();

let indexes = [&external_index[..], &internal_index[..]].concat();

master_key.to_slip32(&KeyPath::default(), Some(indexes))
}

/// Exports the concatenation of the SLIP-32 serialization of the `m_1` and `m_0` keychains, each
/// with 1 extra `u32` field inside the SLIP-32 data holding their own keychain's "next index".
pub fn double_slip32_from_keychains_and_indexes(
m_0: &ExtendedSK,
m_1: &ExtendedSK,
external_index: u32,
internal_index: u32,
) -> Result<String, KeyError> {
let external_index = external_index.to_be_bytes().to_vec();
let internal_index = internal_index.to_be_bytes().to_vec();

let mut internal_secret_key = m_1.to_slip32(&KeyPath::default(), Some(internal_index))?;
let external_secret_key = m_0.to_slip32(&KeyPath::default(), Some(external_index))?;

internal_secret_key.push_str(&external_secret_key);

Ok(internal_secret_key)
}

#[cfg(test)]
mod tests {
use super::*;
Expand Down Expand Up @@ -574,16 +625,34 @@ mod tests {
let seed = mnemonic.seed(&"".into());
let master_key = MasterKeyGen::new(&seed).generate().unwrap();

for (expected, keypath) in slip32_vectors() {
let key = master_key.derive(&keypath).unwrap();
let xprv = key.to_slip32(&keypath).unwrap();
for (expected_xprv, expected_path, expected_extra_data) in slip32_vectors() {
let expected_key = master_key.derive(&expected_path).unwrap();
let xprv = expected_key
.to_slip32(&expected_path, expected_extra_data.clone())
.unwrap();

assert_eq!(expected, xprv);
assert_eq!(expected_xprv, xprv);

let (recovered_key, path) = ExtendedSK::from_slip32(&xprv).unwrap();
let (key, path, extra_data) = ExtendedSK::from_slip32(&xprv).unwrap();

assert_eq!(keypath, path);
assert_eq!(key, recovered_key);
assert_eq!(expected_path, path);
assert_eq!(expected_key, key);
assert_eq!(expected_extra_data, extra_data);
}
}

#[test]
fn test_xprvdouble() {
let phrase = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about";
let mnemonic = bip39::Mnemonic::from_phrase(phrase.into()).unwrap();
let seed = mnemonic.seed(&"".into());
let master_key = MasterKeyGen::new(&seed).generate().unwrap();

let m_0 = master_key.derive(&KeyPath::default().index(0)).unwrap();
let m_1 = master_key.derive(&KeyPath::default().index(1)).unwrap();

let double_hex = double_slip32_from_keychains_and_indexes(&m_0, &m_1, 123, 321).unwrap();

assert_eq!(double_hex, "xprv1qpwy3ytadqutve4wky02clz0nruqwaum2lr4yt3c2zt3nm43u7jeyqxph6hlp3xmnpr8pfqvd8pfg7uax0xh7mn5n3n7rl94ccgcmksjsgqqqq2pwje96dxprv1qrswv5p6cptu7hw8dcrntetd63x3jwewncn3esk5d0r4njvmqg0rcq964zdghhtpch3zh8csvqwc0ywflr7yktaxm7wk35ek7r4s8vrwkcqqqqrm76e6en")
}
}
60 changes: 48 additions & 12 deletions crypto/src/test_vectors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,51 +108,87 @@ pub const TREZOR_MNEMONICS: &[(&str, &str)] = &[
/// abandon abandon abandon abandon abandon abandon about
///
/// See: https://github.com/satoshilabs/slips/blob/master/slip-0032.md#test-vectors
pub fn slip32_vectors() -> Vec<(&'static str, KeyPath)> {
pub fn slip32_vectors() -> Vec<(&'static str, KeyPath, Option<Vec<u8>>)> {
vec![
(
"xprv1qpujxsyd4hfu0dtwa524vac84e09mjsgnh5h9crl8wrqg58z5wmsuqqcxlqmar3fjhkprndzkpnp2xlze76g4hu7g7c4r4r2m2e6y8xlvu566tn6",
KeyPath::default(),
None,
),
(
"xprv1qyqqqqqqurn9qwkq2l84m3mwqu672mw5f5vnkt57yuwv94rtcavunxczrc7qpw4gn29a6cw9ug4e7yrqrkrerj0cl39jlfkln45dxdhsavpmqm4krfqykk",
KeyPath::default().index(0)
KeyPath::default().index(0),
None,
),
(
"xprv1qyqqqqqpt3yfzltg8zmxdt43r6k8cnuclqrh0x6hcafzuwzsjuv7av085kfqpsd74lcvfkucgec2grrfc228h8fne4lkuayuvlsledwxzxxa5y5zefalyg",
KeyPath::default().index(1)
KeyPath::default().index(1),
None,
),
(
"xprv1qxqqqqqq78qr7hlewyyfzt74vasa87k63pu7g9e6hfzlzrdyh0v5k8zfw9sqpsyv7vcejeyzcpkm85jel7vmujlhpquzf4f3sh3nry0w0n4jh7t0jhc039",
KeyPath::default().hardened(0)
KeyPath::default().hardened(0),
None,
),
(
"xprv1qxqqqqqpg0xyhjjecen2taujv52gzfvq9mfva3rd78zu4rn2qkx6k5j6w0csq0hs9lznqqr59zgleyz93w57mjpk8k837fn7xf43q7r3p37mxn095hysnx",
KeyPath::default().hardened(1)
KeyPath::default().hardened(1),
None,
),
(
"xprv1qwqqqqpvsqqqqqyqqqqqq0dyhsvs5f5qzywnr7klmjg972nldnnhcmcsnyv3zme984p5g5seqrlxftuztddhs42vxw3gkgcgtlqg9a53k0r39nqafenwzvef0k585enml6g",
KeyPath::default().hardened(44).hardened(0).hardened(0)
KeyPath::default().hardened(44).hardened(0).hardened(0),
None,
),
(
"xprv1qwqqqqpvsqqqqqyqqqqqz2t3lgkmpl6ad8skdfqxsya28k0dp8z2mtpwpn3n2g7633tqna85qzy9th76xllxvwllcqfkvxzsfc7t6lveyy6xp880vxguw2fnn4wx2mhtjy8",
KeyPath::default().hardened(44).hardened(0).hardened(1)
KeyPath::default().hardened(44).hardened(0).hardened(1),
None,
),
(
"xprv1qwqqqqpvsqqqqq5qqqqqpp5u2pz7tlrcjert40x3jcd3qx7rre6lu5xl3fv9c7dsth9q436cqzvre5gd352pvzcshxjtkceq062c2p22xyekr82hk782n4d2xprdysp4gxc",
KeyPath::default().hardened(44).hardened(2).hardened(0)
KeyPath::default().hardened(44).hardened(2).hardened(0),
None,
),
(
"xprv1qwqqqqp3sqqqqqyqqqqqqm42udj6urs2p24cgvjule7dwmpmjzgrt7yfulflrwz84xs8jlktqzyq65t490dyryrq0cretzxn7ezdj6l6qdzxhn5neh0a50z2n8r7vumvllf",
KeyPath::default().hardened(49).hardened(0).hardened(0)
KeyPath::default().hardened(49).hardened(0).hardened(0),
None,
),
(
"xprv1qwqqqqp3sqqqqq5qqqqqqeahu8w9cu9fx5zzrrx0grz844rdf2wgtqvkxakwp6zn4jnmupycqr8jytxzuztsf8lzefmxymqecln68muhrv0kgx2htz4neqeyv070gg6dcn7",
KeyPath::default().hardened(49).hardened(2).hardened(0)
KeyPath::default().hardened(49).hardened(2).hardened(0),
None,
),
(
"xprv1qwqqqqz5sqqqqqyqqqqqqjjn5z4jrwwujkrfcn5j59s3jnsrcrhnlagpftrf9apnc3m9fy8uqrs57f6dzm9qmygrrwvtzcnpspsaqwfslgup4ak5et6ykqvpn2mdggeaxrp",
KeyPath::default().hardened(84).hardened(0).hardened(0)
)
KeyPath::default().hardened(84).hardened(0).hardened(0),
None,
),
(
"xprv1qpujxsyd4hfu0dtwa524vac84e09mjsgnh5h9crl8wrqg58z5wmsuqqcxlqmar3fjhkprndzkpnp2xlze76g4hu7g7c4r4r2m2e6y8xlvllsrvlvcr",
KeyPath::default(),
Some(vec![255]),
),
(
"xprv1qpujxsyd4hfu0dtwa524vac84e09mjsgnh5h9crl8wrqg58z5wmsuqqcxlqmar3fjhkprndzkpnp2xlze76g4hu7g7c4r4r2m2e6y8xlvuqqqqqqqqqqqqqs0qn8x",
KeyPath::default(),
Some(vec![0; 8]),
),
(
"xprv1qpujxsyd4hfu0dtwa524vac84e09mjsgnh5h9crl8wrqg58z5wmsuqqcxlqmar3fjhkprndzkpnp2xlze76g4hu7g7c4r4r2m2e6y8xlvuqsyqcyq5rqwzqf37gqu5",
KeyPath::default(),
Some(vec![1, 2, 3, 4, 5, 6, 7, 8, 9]),
),
(
"xprv1qpujxsyd4hfu0dtwa524vac84e09mjsgnh5h9crl8wrqg58z5wmsuqqcxlqmar3fjhkprndzkpnp2xlze76g4hu7g7c4r4r2m2e6y8xlvaqyjr7mxq83mn",
KeyPath::default(),
Some(core::f32::consts::PI.to_be_bytes().into()),
),
(
"xprv1qpujxsyd4hfu0dtwa524vac84e09mjsgnh5h9crl8wrqg58z5wmsuqqcxlqmar3fjhkprndzkpnp2xlze76g4hu7g7c4r4r2m2e6y8xlvaqqjg0m23zz6xqh2eujk",
KeyPath::default(),
Some(core::f64::consts::PI.to_be_bytes().into()),
),
]
}
4 changes: 2 additions & 2 deletions data_structures/src/chain/tapi.rs
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ impl TapiEngine {
}
}
for n in 0..self.bit_tapi_counter.len() {
if let Some(mut bit_counter) = self.bit_tapi_counter.get_mut(n, &epoch_to_update) {
if let Some(bit_counter) = self.bit_tapi_counter.get_mut(n, &epoch_to_update) {
if !self.wip_activation.contains_key(&bit_counter.wip)
&& !avoid_wip_list.contains(&bit_counter.wip)
{
Expand Down Expand Up @@ -639,7 +639,7 @@ mod tests {
assert_eq!(tapi_counter.current_length, 1);

assert_eq!(tapi_counter.get(0, &100).unwrap().votes, 0);
let mut votes_counter = tapi_counter.get_mut(0, &100).unwrap();
let votes_counter = tapi_counter.get_mut(0, &100).unwrap();
votes_counter.votes += 1;
assert_eq!(tapi_counter.get(0, &100).unwrap().votes, 1);

Expand Down
2 changes: 1 addition & 1 deletion node/src/actors/inventory_manager/handlers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,7 +512,7 @@ mod tests {
// Start relevant actors
config_mngr::start(config);
storage_mngr::start();
let inventory_manager = InventoryManager::default().start();
let inventory_manager = InventoryManager.start();

// Create first block with value transfer transactions
let block = build_block_with_vt_transactions(1);
Expand Down
2 changes: 1 addition & 1 deletion node/src/actors/json_rpc/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1425,7 +1425,7 @@ pub async fn master_key_export() -> JsonRpcResult {
res.map_err(internal_error)
.and_then(move |(_extended_pk, extended_sk)| {
let master_path = KeyPath::default();
let secret_key_hex = extended_sk.to_slip32(&master_path);
let secret_key_hex = extended_sk.to_slip32(&master_path, None);
let secret_key_hex = match secret_key_hex {
Ok(x) => x,
Err(e) => return Err(internal_error_s(e)),
Expand Down
4 changes: 2 additions & 2 deletions node/src/actors/node.rs
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ pub fn run(config: Arc<Config>, ops: NodeOps, callback: fn()) -> Result<(), fail
SystemRegistry::set(peers_manager_addr);

// Start ConnectionsManager actor
let connections_manager_addr = ConnectionsManager::default().start();
let connections_manager_addr = ConnectionsManager.start();
SystemRegistry::set(connections_manager_addr);

// Start SessionManager actor
Expand All @@ -69,7 +69,7 @@ pub fn run(config: Arc<Config>, ops: NodeOps, callback: fn()) -> Result<(), fail
SystemRegistry::set(chain_manager_addr);

// Start InventoryManager actor
let inventory_manager_addr = InventoryManager::default().start();
let inventory_manager_addr = InventoryManager.start();
SystemRegistry::set(inventory_manager_addr);

// Start RadManager actor
Expand Down
2 changes: 1 addition & 1 deletion node/src/signature_mngr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -595,7 +595,7 @@ fn master_key_import_from_file(master_key_path: &Path) -> Result<ExtendedSK, fai
};

match ExtendedSK::from_slip32(ser.trim()) {
Ok((extended_master_key, key_path)) => {
Ok((extended_master_key, key_path, _)) => {
if key_path.is_master() {
log::info!("Successfully imported master key from file");
Ok(extended_master_key)
Expand Down
2 changes: 1 addition & 1 deletion partial_struct/tests/partial_struct_derive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -81,5 +81,5 @@ fn test_partial_attr_partial() {

let p = PartialObj::default();

assert_eq!(p.f, PartialAnotherObj::default());
assert_eq!(p.f, PartialAnotherObj);
}
8 changes: 7 additions & 1 deletion wallet/src/actors/app/handlers/create_data_req.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ pub struct CreateDataReqRequest {
#[serde(deserialize_with = "deserialize_fee_backwards_compatible")]
fee: Fee,
fee_type: Option<FeeType>,
#[serde(default)]
preview: bool,
}

#[derive(Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -81,7 +83,11 @@ impl Handler<CreateDataReqRequest> for app::App {
let fee = fee_compat(msg.fee, msg.fee_type);

let f = fut::result(validated).and_then(move |request, slf: &mut Self, _ctx| {
let params = types::DataReqParams { request, fee };
let params = types::DataReqParams {
request,
fee,
preview: msg.preview,
};

slf.create_data_req(&msg.session_id, &msg.wallet_id, params)
.map_ok(
Expand Down
Loading