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

BIP39 cosmetics #204

Merged
Merged
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
5 changes: 5 additions & 0 deletions .changes/bip39-better-refs.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"iota-crypto": patch
---

BIP39 reference types and other minor improvements.
139 changes: 105 additions & 34 deletions src/keys/bip39.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,11 @@
// https://doc.rust-lang.org/std/primitive.str.html
// "String slices are always valid UTF-8."

use alloc::borrow::{Borrow, ToOwned};
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use core::convert::TryFrom;
use core::fmt;
use core::ops::Deref;

use unicode_normalization::{is_nfkd, UnicodeNormalization};
Expand Down Expand Up @@ -36,33 +38,36 @@ pub enum Error {
}

/// Reference to a normalized (unicode NFKD) mnemonic.
#[derive(Clone, Copy)]
pub struct MnemonicRef<'a>(&'a str);
#[repr(transparent)]
pub struct MnemonicRef(str);
thibault-martinez marked this conversation as resolved.
Show resolved Hide resolved
thibault-martinez marked this conversation as resolved.
Show resolved Hide resolved

impl<'a> Deref for MnemonicRef<'a> {
impl Deref for MnemonicRef {
type Target = str;
fn deref(&self) -> &str {
self.0
// SAFETY: MnemonicRef is represented exactly as str due to repr(transparent)
unsafe { core::mem::transmute(self) }
thibault-martinez marked this conversation as resolved.
Show resolved Hide resolved
}
}

impl<'a> TryFrom<&'a str> for MnemonicRef<'a> {
impl ToOwned for MnemonicRef {
type Owned = Mnemonic;
fn to_owned(&self) -> Mnemonic {
Mnemonic(self.deref().to_owned())
}
}

impl<'a> TryFrom<&'a str> for &'a MnemonicRef {
type Error = Error;
fn try_from(mnemonic_str: &'a str) -> Result<Self, Error> {
if is_nfkd(mnemonic_str) {
Ok(MnemonicRef(mnemonic_str))
// SAFETY: MnemonicRef is represented exactly as str due to repr(transparent)
Ok(unsafe { core::mem::transmute(mnemonic_str) })
thibault-martinez marked this conversation as resolved.
Show resolved Hide resolved
} else {
Err(Error::UnnormalizedMnemonic)
}
}
}

impl<'a> PartialEq<str> for MnemonicRef<'a> {
fn eq(&self, other: &str) -> bool {
self.0.eq(other)
}
}

/// Owned normalized (unicode NFKD) mnemonic.
///
/// Mnemonic is the encoding of secret entropy using words from a given word list.
Expand All @@ -73,6 +78,20 @@ impl<'a> PartialEq<str> for MnemonicRef<'a> {
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct Mnemonic(String);

impl Deref for Mnemonic {
type Target = MnemonicRef;
fn deref(&self) -> &MnemonicRef {
// SAFETY: MnemonicRef is represented exactly as str due to repr(transparent)
unsafe { core::mem::transmute(self.0.as_str()) }
}
}

impl Borrow<MnemonicRef> for Mnemonic {
fn borrow(&self) -> &MnemonicRef {
self
}
}

/// Normalize the input string and use it as mnemonic.
/// The resulting mnemonic should be verified against a given word list before deriving a seed from it.
impl From<String> for Mnemonic {
Expand Down Expand Up @@ -117,34 +136,50 @@ impl_from_words!(18);
impl_from_words!(21);
impl_from_words!(24);

impl<'a> From<&'a Mnemonic> for MnemonicRef<'a> {
fn from(mnemonic_ref: &'a Mnemonic) -> Self {
Self(&mnemonic_ref.0)
}
}

impl AsRef<str> for Mnemonic {
fn as_ref(&self) -> &str {
&self.0
}
}

impl fmt::Debug for Mnemonic {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
"<bip39::Mnemonic>".fmt(f)
}
}

/// Reference to a normalized (unicode NFKD) passphrase.
#[derive(Clone, Copy)]
pub struct PassphraseRef<'a>(&'a str);
#[repr(transparent)]
pub struct PassphraseRef(str);

impl<'a> Deref for PassphraseRef<'a> {
impl Deref for PassphraseRef {
type Target = str;
fn deref(&self) -> &str {
self.0
// SAFETY: PassphraseRef is represented exactly as str due to repr(transparent)
unsafe { core::mem::transmute(self) }
}
}

impl<'a> TryFrom<&'a str> for PassphraseRef<'a> {
impl ToOwned for PassphraseRef {
type Owned = Passphrase;
fn to_owned(&self) -> Passphrase {
Passphrase(self.deref().to_owned())
}
}

impl<'a> From<&'a Passphrase> for &'a PassphraseRef {
fn from(passphrase_ref: &'a Passphrase) -> Self {
passphrase_ref.borrow()
}
}

impl<'a> TryFrom<&'a str> for &'a PassphraseRef {
type Error = Error;
fn try_from(passphrase_str: &'a str) -> Result<Self, Error> {
if is_nfkd(passphrase_str) {
Ok(PassphraseRef(passphrase_str))
// SAFETY: PassphraseRef is represented exactly as str due to repr(transparent)
Ok(unsafe { core::mem::transmute(passphrase_str) })
} else {
Err(Error::UnnormalizedPassphrase)
}
Expand All @@ -158,15 +193,37 @@ impl<'a> TryFrom<&'a str> for PassphraseRef<'a> {
#[derive(Clone, Zeroize, ZeroizeOnDrop)]
pub struct Passphrase(String);

impl From<String> for Passphrase {
fn from(unnormalized_passphrase: String) -> Self {
Self(unnormalized_passphrase.chars().nfkd().collect())
impl Passphrase {
pub fn new() -> Self {
thibault-martinez marked this conversation as resolved.
Show resolved Hide resolved
Self(String::new())
}
}

impl<'a> From<&'a Passphrase> for PassphraseRef<'a> {
fn from(passphrase_ref: &'a Passphrase) -> Self {
Self(&passphrase_ref.0)
impl Default for Passphrase {
fn default() -> Self {
Self::new()
}
}

impl Deref for Passphrase {
type Target = PassphraseRef;
fn deref(&self) -> &PassphraseRef {
// SAFETY: PassphraseRef is represented exactly as str due to repr(transparent)
unsafe { core::mem::transmute(self.0.as_str()) }
}
}

impl Borrow<PassphraseRef> for Passphrase {
fn borrow(&self) -> &PassphraseRef {
self
}
}

impl From<String> for Passphrase {
fn from(mut unnormalized_passphrase: String) -> Self {
let passphrase = Self(unnormalized_passphrase.chars().nfkd().collect());
unnormalized_passphrase.zeroize();
passphrase
}
}

Expand All @@ -176,6 +233,13 @@ impl AsRef<str> for Passphrase {
}
}

impl fmt::Debug for Passphrase {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
"<bip39::Passphrase>".fmt(f)
}
}

/// Seed is a secret used as master key (ie. other keys are derived/computed from it).
///
/// Seed must either be securely stored (on a hardware token, for example) or it can be derived from mnemonic and
Expand All @@ -195,9 +259,16 @@ impl Seed {
}
}

impl fmt::Debug for Seed {
#[inline]
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
"<bip39::Seed>".fmt(f)
}
}

/// Derive seed from mnemonic and optional (can be empty) passphrase.
// Return seed via mutable reference to avoid potential leaks into stack memory.
pub fn mnemonic_to_seed(m: MnemonicRef, p: PassphraseRef, s: &mut Seed) {
pub fn mnemonic_to_seed(m: &MnemonicRef, p: &PassphraseRef, s: &mut Seed) {
let mut salt = [b"mnemonic", p.0.as_bytes()].concat();
const ROUNDS: core::num::NonZeroU32 = unsafe { core::num::NonZeroU32::new_unchecked(2048) };
crate::keys::pbkdf::PBKDF2_HMAC_SHA512(m.as_bytes(), &salt, ROUNDS, &mut s.0);
Expand Down Expand Up @@ -341,7 +412,7 @@ pub mod wordlist {
/// Be aware that the error detection has a noticable rate of false positives. Given CS
/// checksum bits (CS := ENT / 32) the expected rate of false positives are one in 2^CS. For
/// example given 128 bit entropy that's 1 in 16.
pub fn decode(mnemonic: MnemonicRef, wordlist: &Wordlist) -> Result<Zeroizing<Vec<u8>>, Error> {
pub fn decode(mnemonic: &MnemonicRef, wordlist: &Wordlist) -> Result<Zeroizing<Vec<u8>>, Error> {
// allocate maximal entropy capacity of 32 bytes to avoid reallocations
let mut entropy = Zeroizing::new(Vec::with_capacity(32));

Expand Down Expand Up @@ -387,7 +458,7 @@ pub mod wordlist {
Ok(entropy)
}

pub fn verify(mnemonic: MnemonicRef, wordlist: &Wordlist) -> Result<(), Error> {
pub fn verify(mnemonic: &MnemonicRef, wordlist: &Wordlist) -> Result<(), Error> {
decode(mnemonic, wordlist).map(|_| ())
}
}
Expand All @@ -410,7 +481,7 @@ fn test_encode_decode() {
let n = 4 * i;

let mnemonic = wordlist::encode(&entropy[..n], &wordlist::ENGLISH).unwrap();
let decoded_entropy = wordlist::decode((&mnemonic).into(), &wordlist::ENGLISH).unwrap();
let decoded_entropy = wordlist::decode(&mnemonic, &wordlist::ENGLISH).unwrap();
assert_eq!(&entropy[..n], &decoded_entropy[..]);
}
}
Expand Down
12 changes: 7 additions & 5 deletions src/keys/slip10.rs
Original file line number Diff line number Diff line change
Expand Up @@ -357,18 +357,20 @@ impl<K> Slip10<K> {
}
}

impl<K: hazmat::Derivable> Slip10<K> {
fn key(&self) -> K {
K::to_key(self.key_bytes())
}

impl<K> Slip10<K> {
pub fn extended_bytes(&self) -> &[u8; 65] {
&self.ext
}

pub fn chain_code(&self) -> &[u8; 32] {
unsafe { &*(self.ext[33..].as_ptr() as *const [u8; 32]) }
}
}

impl<K: hazmat::Derivable> Slip10<K> {
fn key(&self) -> K {
K::to_key(self.key_bytes())
}

pub fn try_from_extended_bytes(ext_bytes: &[u8; 65]) -> crate::Result<Self> {
let key_bytes: &[u8; 33] = unsafe { &*(ext_bytes[..33].as_ptr() as *const [u8; 33]) };
Expand Down
15 changes: 8 additions & 7 deletions tests/bip39.rs
Original file line number Diff line number Diff line change
Expand Up @@ -34,20 +34,21 @@ fn test_vectors() {
let entropy = hex::decode(tv.entropy).unwrap();
let mnemonic = hex::decode(tv.mnemonic).unwrap();
let mnemonic = Mnemonic::from(core::str::from_utf8(&mnemonic).unwrap().to_string());
let mnemonic: MnemonicRef = (&mnemonic).into();

assert_eq!(wordlist::encode(&entropy, &tv.wordlist).unwrap().as_ref(), &*mnemonic);
assert_eq!(
wordlist::encode(&entropy, &tv.wordlist).unwrap().as_ref(),
mnemonic.as_ref()
);

assert_eq!(*wordlist::decode(mnemonic, &tv.wordlist).unwrap(), entropy);
assert_eq!(*wordlist::decode(&mnemonic, &tv.wordlist).unwrap(), entropy);

let passphrase = hex::decode(tv.passphrase).unwrap();
let passphrase = Passphrase::from(core::str::from_utf8(&passphrase).unwrap().to_string());
let passphrase: PassphraseRef = (&passphrase).into();
let mut expected_seed = [0; 64];
hex::decode_to_slice(tv.seed, &mut expected_seed).unwrap();

let mut seed = Seed::null();
mnemonic_to_seed(mnemonic, passphrase, &mut seed);
mnemonic_to_seed(&mnemonic, &passphrase, &mut seed);
assert_eq!(seed.as_ref(), &expected_seed);
}
}
Expand Down Expand Up @@ -82,8 +83,8 @@ fn test_wordlist_codec() {

for ws in ALL_WORDLISTS {
let ms = wordlist::encode(&data, ws).unwrap();
assert_eq!(*wordlist::decode((&ms).into(), ws).unwrap(), data);
assert_eq!(wordlist::verify((&ms).into(), ws), Ok(()));
assert_eq!(*wordlist::decode(&ms, ws).unwrap(), data);
assert_eq!(wordlist::verify(&ms, ws), Ok(()));
}
}
}
Expand Down