feat(wallet): implement HD private keys & encrypted wrapper (#358)
This commit is contained in:
parent
8b13646680
commit
550eac147b
6 changed files with 413 additions and 175 deletions
|
|
@ -18,6 +18,7 @@ pallas-crypto = { path = "../pallas-crypto", version = "=0.20.0" }
|
||||||
pallas-primitives = { path = "../pallas-primitives", version = "=0.20.0" }
|
pallas-primitives = { path = "../pallas-primitives", version = "=0.20.0" }
|
||||||
pallas-traverse = { path = "../pallas-traverse", version = "=0.20.0" }
|
pallas-traverse = { path = "../pallas-traverse", version = "=0.20.0" }
|
||||||
pallas-addresses = { path = "../pallas-addresses", version = "=0.20.0" }
|
pallas-addresses = { path = "../pallas-addresses", version = "=0.20.0" }
|
||||||
|
pallas-wallet = { path = "../pallas-wallet", version = "=0.20.0" }
|
||||||
serde = { version = "1.0.188", features = ["derive"] }
|
serde = { version = "1.0.188", features = ["derive"] }
|
||||||
serde_json = "1.0.107"
|
serde_json = "1.0.107"
|
||||||
thiserror = "1.0.44"
|
thiserror = "1.0.44"
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ use pallas_crypto::{
|
||||||
key::ed25519,
|
key::ed25519,
|
||||||
};
|
};
|
||||||
use pallas_primitives::{babbage, Fragment};
|
use pallas_primitives::{babbage, Fragment};
|
||||||
|
use pallas_wallet::PrivateKey;
|
||||||
|
|
||||||
use std::{collections::HashMap, ops::Deref};
|
use std::{collections::HashMap, ops::Deref};
|
||||||
|
|
||||||
|
|
@ -608,14 +609,14 @@ pub struct BuiltTransaction {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl BuiltTransaction {
|
impl BuiltTransaction {
|
||||||
pub fn sign(mut self, secret_key: ed25519::SecretKey) -> Result<Self, TxBuilderError> {
|
pub fn sign(mut self, private_key: PrivateKey) -> Result<Self, TxBuilderError> {
|
||||||
let pubkey: [u8; 32] = secret_key
|
let pubkey: [u8; 32] = private_key
|
||||||
.public_key()
|
.public_key()
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.try_into()
|
.try_into()
|
||||||
.map_err(|_| TxBuilderError::MalformedKey)?;
|
.map_err(|_| TxBuilderError::MalformedKey)?;
|
||||||
|
|
||||||
let signature: [u8; 64] = secret_key.sign(self.tx_hash.0).as_ref().try_into().unwrap();
|
let signature: [u8; ed25519::Signature::SIZE] = private_key.sign(self.tx_hash.0).as_ref().try_into().unwrap();
|
||||||
|
|
||||||
match self.era {
|
match self.era {
|
||||||
BuilderEra::Babbage => {
|
BuilderEra::Babbage => {
|
||||||
|
|
|
||||||
|
|
@ -16,3 +16,4 @@ ed25519-bip32 = "0.4.1"
|
||||||
bip39 = { version = "2.0.0", features = ["rand_core"] }
|
bip39 = { version = "2.0.0", features = ["rand_core"] }
|
||||||
cryptoxide = "0.4.4"
|
cryptoxide = "0.4.4"
|
||||||
bech32 = "0.9.1"
|
bech32 = "0.9.1"
|
||||||
|
rand = "0.8.5"
|
||||||
|
|
|
||||||
192
pallas-wallet/src/hd.rs
Normal file
192
pallas-wallet/src/hd.rs
Normal file
|
|
@ -0,0 +1,192 @@
|
||||||
|
use bech32::{FromBase32, ToBase32};
|
||||||
|
use bip39::rand_core::{CryptoRng, RngCore};
|
||||||
|
use bip39::{Language, Mnemonic};
|
||||||
|
use cryptoxide::{hmac::Hmac, pbkdf2::pbkdf2, sha2::Sha512};
|
||||||
|
use ed25519_bip32::{self, XPrv, XPub, XPRV_SIZE};
|
||||||
|
use pallas_crypto::key::ed25519;
|
||||||
|
|
||||||
|
use crate::{Error, PrivateKey};
|
||||||
|
|
||||||
|
/// Ed25519-BIP32 HD Private Key
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct Bip32PrivateKey(ed25519_bip32::XPrv);
|
||||||
|
|
||||||
|
impl Bip32PrivateKey {
|
||||||
|
const BECH32_HRP: &'static str = "xprv";
|
||||||
|
|
||||||
|
pub fn generate<T: RngCore + CryptoRng>(mut rng: T) -> Self {
|
||||||
|
let mut buf = [0u8; XPRV_SIZE];
|
||||||
|
rng.fill_bytes(&mut buf);
|
||||||
|
let xprv = XPrv::normalize_bytes_force3rd(buf);
|
||||||
|
|
||||||
|
Self(xprv)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_with_mnemonic<T: RngCore + CryptoRng>(
|
||||||
|
mut rng: T,
|
||||||
|
password: String,
|
||||||
|
) -> (Self, Mnemonic) {
|
||||||
|
let mut buf = [0u8; 64];
|
||||||
|
rng.fill_bytes(&mut buf);
|
||||||
|
|
||||||
|
let bip39 = Mnemonic::generate_in_with(&mut rng, Language::English, 24).unwrap();
|
||||||
|
|
||||||
|
let entropy = bip39.clone().to_entropy();
|
||||||
|
|
||||||
|
let mut pbkdf2_result = [0; XPRV_SIZE];
|
||||||
|
|
||||||
|
const ITER: u32 = 4096; // TODO: BIP39 says 2048, CML uses 4096?
|
||||||
|
|
||||||
|
let mut mac = Hmac::new(Sha512::new(), password.as_bytes());
|
||||||
|
pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result);
|
||||||
|
|
||||||
|
(Self(XPrv::normalize_bytes_force3rd(pbkdf2_result)), bip39)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bytes(bytes: [u8; 96]) -> Result<Self, Error> {
|
||||||
|
XPrv::from_bytes_verified(bytes)
|
||||||
|
.map(Self)
|
||||||
|
.map_err(Error::Xprv)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_bytes(&self) -> Vec<u8> {
|
||||||
|
self.0.as_ref().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bip39_mnenomic(mnemonic: String, password: String) -> Result<Self, Error> {
|
||||||
|
let bip39 = Mnemonic::parse(mnemonic).map_err(Error::Mnemonic)?;
|
||||||
|
let entropy = bip39.to_entropy();
|
||||||
|
|
||||||
|
let mut pbkdf2_result = [0; XPRV_SIZE];
|
||||||
|
|
||||||
|
const ITER: u32 = 4096; // TODO: BIP39 says 2048, CML uses 4096?
|
||||||
|
|
||||||
|
let mut mac = Hmac::new(Sha512::new(), password.as_bytes());
|
||||||
|
pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result);
|
||||||
|
|
||||||
|
Ok(Self(XPrv::normalize_bytes_force3rd(pbkdf2_result)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn derive(&self, index: u32) -> Self {
|
||||||
|
Self(self.0.derive(ed25519_bip32::DerivationScheme::V2, index))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_ed25519_private_key(&self) -> PrivateKey {
|
||||||
|
PrivateKey::Extended(self.0.extended_secret_key().into())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_public(&self) -> Bip32PublicKey {
|
||||||
|
Bip32PublicKey(self.0.public())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chain_code(&self) -> [u8; 32] {
|
||||||
|
*self.0.chain_code()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_bech32(&self) -> String {
|
||||||
|
bech32::encode(
|
||||||
|
Self::BECH32_HRP,
|
||||||
|
self.as_bytes().to_base32(),
|
||||||
|
bech32::Variant::Bech32,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bech32(bech32: String) -> Result<Self, Error> {
|
||||||
|
let (hrp, data, _) = bech32::decode(&bech32).map_err(Error::InvalidBech32)?;
|
||||||
|
if hrp != Self::BECH32_HRP {
|
||||||
|
Err(Error::InvalidBech32Hrp)
|
||||||
|
} else {
|
||||||
|
let data = Vec::<u8>::from_base32(&data).map_err(Error::InvalidBech32)?;
|
||||||
|
Self::from_bytes(data.try_into().map_err(|_| Error::UnexpectedBech32Length)?)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Ed25519-BIP32 HD Public Key
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub struct Bip32PublicKey(ed25519_bip32::XPub);
|
||||||
|
|
||||||
|
impl Bip32PublicKey {
|
||||||
|
const BECH32_HRP: &'static str = "xpub";
|
||||||
|
|
||||||
|
pub fn from_bytes(bytes: [u8; 64]) -> Self {
|
||||||
|
Self(XPub::from_bytes(bytes))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn as_bytes(&self) -> Vec<u8> {
|
||||||
|
self.0.as_ref().to_vec()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn derive(&self, index: u32) -> Result<Self, Error> {
|
||||||
|
self.0
|
||||||
|
.derive(ed25519_bip32::DerivationScheme::V2, index)
|
||||||
|
.map(Self)
|
||||||
|
.map_err(Error::DerivationError)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_ed25519_pubkey(&self) -> ed25519::PublicKey {
|
||||||
|
self.0.public_key().into()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn chain_code(&self) -> [u8; 32] {
|
||||||
|
*self.0.chain_code()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn to_bech32(&self) -> String {
|
||||||
|
bech32::encode(
|
||||||
|
Self::BECH32_HRP,
|
||||||
|
self.as_bytes().to_base32(),
|
||||||
|
bech32::Variant::Bech32,
|
||||||
|
)
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn from_bech32(bech32: String) -> Result<Self, Error> {
|
||||||
|
let (hrp, data, _) = bech32::decode(&bech32).map_err(Error::InvalidBech32)?;
|
||||||
|
if hrp != Self::BECH32_HRP {
|
||||||
|
Err(Error::InvalidBech32Hrp)
|
||||||
|
} else {
|
||||||
|
let data = Vec::<u8>::from_base32(&data).map_err(Error::InvalidBech32)?;
|
||||||
|
Ok(Self::from_bytes(
|
||||||
|
data.try_into().map_err(|_| Error::UnexpectedBech32Length)?,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod test {
|
||||||
|
use bip39::rand_core::OsRng;
|
||||||
|
|
||||||
|
use super::{Bip32PrivateKey, Bip32PublicKey};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn mnemonic_roundtrip() {
|
||||||
|
let (xprv, mne) = Bip32PrivateKey::generate_with_mnemonic(OsRng, "".into());
|
||||||
|
|
||||||
|
let xprv_from_mne =
|
||||||
|
Bip32PrivateKey::from_bip39_mnenomic(mne.to_string(), "".into()).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(xprv, xprv_from_mne)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn bech32_roundtrip() {
|
||||||
|
let xprv = Bip32PrivateKey::generate(OsRng);
|
||||||
|
|
||||||
|
let xprv_bech32 = xprv.to_bech32();
|
||||||
|
|
||||||
|
let decoded_xprv = Bip32PrivateKey::from_bech32(xprv_bech32).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(xprv, decoded_xprv);
|
||||||
|
|
||||||
|
let xpub = xprv.to_public();
|
||||||
|
|
||||||
|
let xpub_bech32 = xpub.to_bech32();
|
||||||
|
|
||||||
|
let decoded_xpub = Bip32PublicKey::from_bech32(xpub_bech32).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(xpub, decoded_xpub)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,13 +1,18 @@
|
||||||
use bech32::{FromBase32, ToBase32};
|
use ed25519_bip32;
|
||||||
use bip39::rand_core::{CryptoRng, RngCore};
|
use pallas_crypto::key::ed25519::{PublicKey, SecretKey, SecretKeyExtended, Signature};
|
||||||
use bip39::{Language, Mnemonic};
|
|
||||||
use cryptoxide::{hmac::Hmac, pbkdf2::pbkdf2, sha2::Sha512};
|
|
||||||
use ed25519_bip32::{self, XPrv, XPub, XPRV_SIZE};
|
|
||||||
use pallas_crypto::key::ed25519::{self};
|
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
|
pub mod hd;
|
||||||
|
pub mod wrapper;
|
||||||
|
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub enum Error {
|
pub enum Error {
|
||||||
|
/// Private key wrapper data of unexpected length
|
||||||
|
#[error("Wrapped private key data invalid length")]
|
||||||
|
WrapperDataInvalidSize,
|
||||||
|
/// Failed to decrypt private key wrapper data
|
||||||
|
#[error("Failed to decrypt private key wrapper data")]
|
||||||
|
WrapperDataFailedToDecrypt,
|
||||||
/// Unexpected bech32 HRP prefix
|
/// Unexpected bech32 HRP prefix
|
||||||
#[error("Unexpected bech32 HRP prefix")]
|
#[error("Unexpected bech32 HRP prefix")]
|
||||||
InvalidBech32Hrp,
|
InvalidBech32Hrp,
|
||||||
|
|
@ -28,186 +33,59 @@ pub enum Error {
|
||||||
DerivationError(ed25519_bip32::DerivationError),
|
DerivationError(ed25519_bip32::DerivationError),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ED25519-BIP32 HD Private Key
|
/// A standard or extended Ed25519 secret key
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
pub enum PrivateKey {
|
||||||
pub struct Bip32PrivateKey(ed25519_bip32::XPrv);
|
Normal(SecretKey),
|
||||||
|
Extended(SecretKeyExtended),
|
||||||
|
}
|
||||||
|
|
||||||
impl Bip32PrivateKey {
|
impl PrivateKey {
|
||||||
const BECH32_HRP: &'static str = "xprv";
|
pub fn len(&self) -> usize {
|
||||||
|
match self {
|
||||||
pub fn generate<T: RngCore + CryptoRng>(mut rng: T) -> Self {
|
Self::Normal(_) => SecretKey::SIZE,
|
||||||
let mut buf = [0u8; XPRV_SIZE];
|
Self::Extended(_) => SecretKeyExtended::SIZE,
|
||||||
rng.fill_bytes(&mut buf);
|
}
|
||||||
let xprv = XPrv::normalize_bytes_force3rd(buf);
|
|
||||||
|
|
||||||
Self(xprv)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn generate_with_mnemonic<T: RngCore + CryptoRng>(
|
pub fn public_key(&self) -> PublicKey {
|
||||||
mut rng: T,
|
match self {
|
||||||
password: String,
|
Self::Normal(x) => x.public_key(),
|
||||||
) -> (Self, Mnemonic) {
|
Self::Extended(x) => x.public_key(),
|
||||||
let mut buf = [0u8; 64];
|
}
|
||||||
rng.fill_bytes(&mut buf);
|
|
||||||
|
|
||||||
let bip39 = Mnemonic::generate_in_with(&mut rng, Language::English, 24).unwrap();
|
|
||||||
|
|
||||||
let entropy = bip39.clone().to_entropy();
|
|
||||||
|
|
||||||
let mut pbkdf2_result = [0; XPRV_SIZE];
|
|
||||||
|
|
||||||
const ITER: u32 = 4096; // TODO: BIP39 says 2048, CML uses 4096?
|
|
||||||
|
|
||||||
let mut mac = Hmac::new(Sha512::new(), password.as_bytes());
|
|
||||||
pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result);
|
|
||||||
|
|
||||||
(Self(XPrv::normalize_bytes_force3rd(pbkdf2_result)), bip39)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn from_bytes(bytes: [u8; 96]) -> Result<Self, Error> {
|
pub fn sign<T>(&self, msg: T) -> Signature
|
||||||
XPrv::from_bytes_verified(bytes)
|
where
|
||||||
.map(Self)
|
T: AsRef<[u8]>,
|
||||||
.map_err(Error::Xprv)
|
{
|
||||||
|
match self {
|
||||||
|
Self::Normal(x) => x.sign(msg),
|
||||||
|
Self::Extended(x) => x.sign(msg),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn as_bytes(&self) -> Vec<u8> {
|
pub fn as_bytes(&self) -> Vec<u8> {
|
||||||
self.0.as_ref().to_vec()
|
match self {
|
||||||
}
|
Self::Normal(x) => {
|
||||||
|
let bytes: [u8; SecretKey::SIZE] = x.clone().into();
|
||||||
pub fn from_bip39_mnenomic(mnemonic: String, password: String) -> Result<Self, Error> {
|
bytes.to_vec()
|
||||||
let bip39 = Mnemonic::parse(mnemonic).map_err(Error::Mnemonic)?;
|
}
|
||||||
let entropy = bip39.to_entropy();
|
Self::Extended(x) => {
|
||||||
|
let bytes: [u8; SecretKeyExtended::SIZE] = x.clone().into();
|
||||||
let mut pbkdf2_result = [0; XPRV_SIZE];
|
bytes.to_vec()
|
||||||
|
}
|
||||||
const ITER: u32 = 4096; // TODO: BIP39 says 2048, CML uses 4096?
|
|
||||||
|
|
||||||
let mut mac = Hmac::new(Sha512::new(), password.as_bytes());
|
|
||||||
pbkdf2(&mut mac, &entropy, ITER, &mut pbkdf2_result);
|
|
||||||
|
|
||||||
Ok(Self(XPrv::normalize_bytes_force3rd(pbkdf2_result)))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn derive(&self, index: u32) -> Self {
|
|
||||||
Self(self.0.derive(ed25519_bip32::DerivationScheme::V2, index))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_ed25519_privkey(&self) -> ed25519::SecretKeyExtended {
|
|
||||||
self.0.extended_secret_key().into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_public(&self) -> Bip32PublicKey {
|
|
||||||
Bip32PublicKey(self.0.public())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn chain_code(&self) -> [u8; 32] {
|
|
||||||
*self.0.chain_code()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_bech32(&self) -> String {
|
|
||||||
bech32::encode(
|
|
||||||
Self::BECH32_HRP,
|
|
||||||
self.as_bytes().to_base32(),
|
|
||||||
bech32::Variant::Bech32,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_bech32(bech32: String) -> Result<Self, Error> {
|
|
||||||
let (hrp, data, _) = bech32::decode(&bech32).map_err(Error::InvalidBech32)?;
|
|
||||||
if hrp != Self::BECH32_HRP {
|
|
||||||
Err(Error::InvalidBech32Hrp)
|
|
||||||
} else {
|
|
||||||
let data = Vec::<u8>::from_base32(&data).map_err(Error::InvalidBech32)?;
|
|
||||||
Self::from_bytes(data.try_into().map_err(|_| Error::UnexpectedBech32Length)?)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// ED25519-BIP32 HD Public Key
|
impl From<SecretKey> for PrivateKey {
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
fn from(key: SecretKey) -> Self {
|
||||||
pub struct Bip32PublicKey(ed25519_bip32::XPub);
|
PrivateKey::Normal(key)
|
||||||
|
|
||||||
impl Bip32PublicKey {
|
|
||||||
const BECH32_HRP: &'static str = "xpub";
|
|
||||||
|
|
||||||
pub fn from_bytes(bytes: [u8; 64]) -> Self {
|
|
||||||
Self(XPub::from_bytes(bytes))
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn as_bytes(&self) -> Vec<u8> {
|
|
||||||
self.0.as_ref().to_vec()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn derive(&self, index: u32) -> Result<Self, Error> {
|
|
||||||
self.0
|
|
||||||
.derive(ed25519_bip32::DerivationScheme::V2, index)
|
|
||||||
.map(Self)
|
|
||||||
.map_err(Error::DerivationError)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_ed25519_pubkey(&self) -> ed25519::PublicKey {
|
|
||||||
self.0.public_key().into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn chain_code(&self) -> [u8; 32] {
|
|
||||||
*self.0.chain_code()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn to_bech32(&self) -> String {
|
|
||||||
bech32::encode(
|
|
||||||
Self::BECH32_HRP,
|
|
||||||
self.as_bytes().to_base32(),
|
|
||||||
bech32::Variant::Bech32,
|
|
||||||
)
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn from_bech32(bech32: String) -> Result<Self, Error> {
|
|
||||||
let (hrp, data, _) = bech32::decode(&bech32).map_err(Error::InvalidBech32)?;
|
|
||||||
if hrp != Self::BECH32_HRP {
|
|
||||||
Err(Error::InvalidBech32Hrp)
|
|
||||||
} else {
|
|
||||||
let data = Vec::<u8>::from_base32(&data).map_err(Error::InvalidBech32)?;
|
|
||||||
Ok(Self::from_bytes(
|
|
||||||
data.try_into().map_err(|_| Error::UnexpectedBech32Length)?,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
impl From<SecretKeyExtended> for PrivateKey {
|
||||||
mod test {
|
fn from(key: SecretKeyExtended) -> Self {
|
||||||
use bip39::rand_core::OsRng;
|
PrivateKey::Extended(key)
|
||||||
|
|
||||||
use crate::{Bip32PrivateKey, Bip32PublicKey};
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn mnemonic_roundtrip() {
|
|
||||||
let (xprv, mne) = Bip32PrivateKey::generate_with_mnemonic(OsRng, "".into());
|
|
||||||
|
|
||||||
let xprv_from_mne =
|
|
||||||
Bip32PrivateKey::from_bip39_mnenomic(mne.to_string(), "".into()).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(xprv, xprv_from_mne)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn bech32_roundtrip() {
|
|
||||||
let xprv = Bip32PrivateKey::generate(OsRng);
|
|
||||||
|
|
||||||
let xprv_bech32 = xprv.to_bech32();
|
|
||||||
|
|
||||||
let decoded_xprv = Bip32PrivateKey::from_bech32(xprv_bech32).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(xprv, decoded_xprv);
|
|
||||||
|
|
||||||
let xpub = xprv.to_public();
|
|
||||||
|
|
||||||
let xpub_bech32 = xpub.to_bech32();
|
|
||||||
|
|
||||||
let decoded_xpub = Bip32PublicKey::from_bech32(xpub_bech32).unwrap();
|
|
||||||
|
|
||||||
assert_eq!(xpub, decoded_xpub)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
165
pallas-wallet/src/wrapper.rs
Normal file
165
pallas-wallet/src/wrapper.rs
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
use cryptoxide::chacha20poly1305::ChaCha20Poly1305;
|
||||||
|
use cryptoxide::kdf::argon2;
|
||||||
|
use pallas_crypto::key::ed25519::{SecretKey, SecretKeyExtended};
|
||||||
|
use rand::{CryptoRng, RngCore};
|
||||||
|
|
||||||
|
use crate::{Error, PrivateKey};
|
||||||
|
|
||||||
|
const ITERATIONS: u32 = 2500;
|
||||||
|
const VERSION_SIZE: usize = 1;
|
||||||
|
const SALT_SIZE: usize = 16;
|
||||||
|
const NONCE_SIZE: usize = 12;
|
||||||
|
const TAG_SIZE: usize = 16;
|
||||||
|
|
||||||
|
pub fn encrypt_private_key<Rng>(mut rng: Rng, private_key: PrivateKey, password: &String) -> Vec<u8>
|
||||||
|
where
|
||||||
|
Rng: RngCore + CryptoRng,
|
||||||
|
{
|
||||||
|
let salt = {
|
||||||
|
let mut salt = [0u8; SALT_SIZE];
|
||||||
|
rng.fill_bytes(&mut salt);
|
||||||
|
salt
|
||||||
|
};
|
||||||
|
|
||||||
|
let sym_key: [u8; 32] = argon2::argon2(
|
||||||
|
&argon2::Params::argon2d().iterations(ITERATIONS).unwrap(),
|
||||||
|
password.as_bytes(),
|
||||||
|
&salt,
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
|
||||||
|
let nonce = {
|
||||||
|
let mut nonce = [0u8; NONCE_SIZE];
|
||||||
|
rng.fill_bytes(&mut nonce);
|
||||||
|
nonce
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut chacha20 = ChaCha20Poly1305::new(&sym_key, &nonce, &[]);
|
||||||
|
|
||||||
|
let data_size = private_key.len();
|
||||||
|
|
||||||
|
let (ciphertext, ct_tag) = {
|
||||||
|
let mut ciphertext = vec![0u8; data_size];
|
||||||
|
let mut ct_tag = [0u8; 16];
|
||||||
|
chacha20.encrypt(&private_key.as_bytes(), &mut ciphertext, &mut ct_tag);
|
||||||
|
|
||||||
|
(ciphertext, ct_tag)
|
||||||
|
};
|
||||||
|
|
||||||
|
// (version || salt || nonce || tag || ciphertext)
|
||||||
|
let mut out = Vec::with_capacity(VERSION_SIZE + SALT_SIZE + NONCE_SIZE + TAG_SIZE + data_size);
|
||||||
|
|
||||||
|
out.push(1);
|
||||||
|
out.extend_from_slice(&salt);
|
||||||
|
out.extend_from_slice(&nonce);
|
||||||
|
out.extend_from_slice(&ct_tag);
|
||||||
|
out.extend_from_slice(&ciphertext);
|
||||||
|
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
#[allow(unused)]
|
||||||
|
pub fn decrypt_private_key(password: &String, data: Vec<u8>) -> Result<PrivateKey, Error> {
|
||||||
|
let data_len_without_ct = VERSION_SIZE + SALT_SIZE + NONCE_SIZE + TAG_SIZE;
|
||||||
|
|
||||||
|
let ciphertext_len = if data.len() == (data_len_without_ct + SecretKey::SIZE) {
|
||||||
|
SecretKey::SIZE
|
||||||
|
} else if data.len() == (data_len_without_ct + SecretKeyExtended::SIZE) {
|
||||||
|
SecretKeyExtended::SIZE
|
||||||
|
} else {
|
||||||
|
return Err(Error::WrapperDataInvalidSize);
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut cursor = 0;
|
||||||
|
|
||||||
|
let _version = &data[cursor];
|
||||||
|
cursor += VERSION_SIZE;
|
||||||
|
|
||||||
|
let salt = &data[cursor..cursor + SALT_SIZE];
|
||||||
|
cursor += SALT_SIZE;
|
||||||
|
|
||||||
|
let nonce = &data[cursor..cursor + NONCE_SIZE];
|
||||||
|
cursor += NONCE_SIZE;
|
||||||
|
|
||||||
|
let tag = &data[cursor..cursor + TAG_SIZE];
|
||||||
|
cursor += TAG_SIZE;
|
||||||
|
|
||||||
|
let ciphertext = &data[cursor..cursor + ciphertext_len];
|
||||||
|
|
||||||
|
let sym_key: [u8; 32] = argon2::argon2(
|
||||||
|
&argon2::Params::argon2d().iterations(ITERATIONS).unwrap(),
|
||||||
|
password.as_bytes(),
|
||||||
|
salt,
|
||||||
|
&[],
|
||||||
|
&[],
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut chacha20 = ChaCha20Poly1305::new(&sym_key, nonce, &[]);
|
||||||
|
|
||||||
|
match ciphertext_len {
|
||||||
|
SecretKey::SIZE => {
|
||||||
|
let mut plaintext = [0u8; SecretKey::SIZE];
|
||||||
|
|
||||||
|
if chacha20.decrypt(ciphertext, &mut plaintext, tag) {
|
||||||
|
let secret_key: SecretKey = plaintext.into();
|
||||||
|
|
||||||
|
Ok(secret_key.into())
|
||||||
|
} else {
|
||||||
|
Err(Error::WrapperDataFailedToDecrypt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
SecretKeyExtended::SIZE => {
|
||||||
|
let mut plaintext = [0u8; SecretKeyExtended::SIZE];
|
||||||
|
|
||||||
|
if chacha20.decrypt(ciphertext, &mut plaintext, tag) {
|
||||||
|
let secret_key: SecretKeyExtended = plaintext.into();
|
||||||
|
|
||||||
|
Ok(secret_key.into())
|
||||||
|
} else {
|
||||||
|
Err(Error::WrapperDataFailedToDecrypt)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => unreachable!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use pallas_crypto::key::ed25519::{SecretKey, SecretKeyExtended};
|
||||||
|
use rand::rngs::OsRng;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
wrapper::{decrypt_private_key, encrypt_private_key},
|
||||||
|
PrivateKey,
|
||||||
|
};
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn private_key_encryption_roundtrip() {
|
||||||
|
let password = "hunter123";
|
||||||
|
|
||||||
|
// --- standard
|
||||||
|
|
||||||
|
let private_key = PrivateKey::Normal(SecretKey::new(OsRng));
|
||||||
|
|
||||||
|
let private_key_bytes = private_key.as_bytes();
|
||||||
|
|
||||||
|
let encrypted_priv_key = encrypt_private_key(OsRng, private_key, &password.into());
|
||||||
|
|
||||||
|
let decrypted_privkey = decrypt_private_key(&password.into(), encrypted_priv_key).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(private_key_bytes, decrypted_privkey.as_bytes());
|
||||||
|
|
||||||
|
// --- extended
|
||||||
|
|
||||||
|
let private_key = PrivateKey::Extended(SecretKeyExtended::new(OsRng));
|
||||||
|
|
||||||
|
let private_key_bytes = private_key.as_bytes();
|
||||||
|
|
||||||
|
let encrypted_priv_key = encrypt_private_key(OsRng, private_key, &password.into());
|
||||||
|
|
||||||
|
let decrypted_privkey = decrypt_private_key(&password.into(), encrypted_priv_key).unwrap();
|
||||||
|
|
||||||
|
assert_eq!(private_key_bytes, decrypted_privkey.as_bytes())
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue