Skip to content

Commit

Permalink
feat: add ffi get mnemonic wordlist (#3538)
Browse files Browse the repository at this point in the history
Description
---
Added an FFI method to get the entire mnemonic word list for a given language.

Motivation and Context
---
See above.

How Has This Been Tested?
---
- Added a unit test to test this functionality.
- Added a cucumber test to verify the mnemonic word list is retrieved.
  • Loading branch information
hansieodendaal committed Nov 9, 2021
1 parent 23e868a commit d8e0ced
Show file tree
Hide file tree
Showing 15 changed files with 731 additions and 4,536 deletions.
29 changes: 26 additions & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions base_layer/key_manager/Cargo.toml
Expand Up @@ -20,6 +20,8 @@ crc32fast = "1.2.1"
rand = "0.8"
digest = "0.9.0"
thiserror = "1.0.26"
strum_macros = "0.22"
strum = { version = "0.22", features = ["derive"] }

[dev-dependencies]
sha2 = "0.9.8"
Expand Down
40 changes: 39 additions & 1 deletion base_layer/key_manager/src/mnemonic.rs
Expand Up @@ -26,13 +26,14 @@ use crate::{
mnemonic_wordlists::*,
};
use std::slice::Iter;
use strum_macros::{Display, EnumString};
use tari_crypto::tari_utilities::bit::*;

/// The Mnemonic system simplifies the encoding and decoding of a secret key into and from a Mnemonic word sequence
/// It can autodetect the language of the Mnemonic word sequence
// TODO: Develop a language autodetection mechanism to distinguish between ChineseTraditional and ChineseSimplified

#[derive(Clone, Debug, PartialEq)]
#[derive(Clone, Debug, PartialEq, EnumString, Display)]
pub enum MnemonicLanguage {
ChineseSimplified,
English,
Expand Down Expand Up @@ -67,6 +68,19 @@ impl MnemonicLanguage {
];
MNEMONIC_LANGUAGES.iter()
}

/// Returns the mnemonic word list count for the specified language
pub fn word_count(language: &MnemonicLanguage) -> usize {
match language {
MnemonicLanguage::ChineseSimplified => MNEMONIC_CHINESE_SIMPLIFIED_WORDS.len(),
MnemonicLanguage::English => MNEMONIC_ENGLISH_WORDS.len(),
MnemonicLanguage::French => MNEMONIC_FRENCH_WORDS.len(),
MnemonicLanguage::Italian => MNEMONIC_ITALIAN_WORDS.len(),
MnemonicLanguage::Japanese => MNEMONIC_JAPANESE_WORDS.len(),
MnemonicLanguage::Korean => MNEMONIC_KOREAN_WORDS.len(),
MnemonicLanguage::Spanish => MNEMONIC_SPANISH_WORDS.len(),
}
}
}

/// Finds and returns the index of a specific word in a mnemonic word list defined by the specified language
Expand Down Expand Up @@ -193,6 +207,7 @@ mod test {
use super::*;
use crate::mnemonic;
use rand::{self, rngs::OsRng};
use std::str::FromStr;
use tari_crypto::{keys::SecretKey, ristretto::RistrettoSecretKey, tari_utilities::byte_array::ByteArray};

#[test]
Expand All @@ -211,6 +226,29 @@ mod test {
}
}

#[test]
fn test_string_to_enum_conversion() {
let my_enum = MnemonicLanguage::from_str("ChineseSimplified").unwrap();
assert_eq!(my_enum, MnemonicLanguage::ChineseSimplified);
let my_enum = MnemonicLanguage::from_str("English").unwrap();
assert_eq!(my_enum, MnemonicLanguage::English);
let my_enum = MnemonicLanguage::from_str("French").unwrap();
assert_eq!(my_enum, MnemonicLanguage::French);
let my_enum = MnemonicLanguage::from_str("Italian").unwrap();
assert_eq!(my_enum, MnemonicLanguage::Italian);
let my_enum = MnemonicLanguage::from_str("Japanese").unwrap();
assert_eq!(my_enum, MnemonicLanguage::Japanese);
let my_enum = MnemonicLanguage::from_str("Korean").unwrap();
assert_eq!(my_enum, MnemonicLanguage::Korean);
let my_enum = MnemonicLanguage::from_str("Spanish").unwrap();
assert_eq!(my_enum, MnemonicLanguage::Spanish);
let my_language = "TariVerse";
match MnemonicLanguage::from_str(my_language) {
Ok(_) => panic!("Language '{}' is not a member of 'MnemonicLanguage'!", my_language),
Err(e) => assert_eq!(e, strum::ParseError::VariantNotFound),
}
}

#[test]
fn test_language_detection() {
// Test valid Mnemonic words
Expand Down
1 change: 1 addition & 0 deletions base_layer/wallet_ffi/src/enums.rs
Expand Up @@ -26,4 +26,5 @@ pub enum SeedWordPushResult {
SuccessfulPush,
SeedPhraseComplete,
InvalidSeedPhrase,
InvalidObject,
}
6 changes: 6 additions & 0 deletions base_layer/wallet_ffi/src/error.rs
Expand Up @@ -52,6 +52,8 @@ pub enum InterfaceError {
NetworkError(String),
#[error("Emoji ID is invalid")]
InvalidEmojiId,
#[error("An error has occurred due to an invalid argument: `{0}`")]
InvalidArgument(String),
}

/// This struct is meant to hold an error for use by FFI client applications. The error has an integer code and string
Expand Down Expand Up @@ -90,6 +92,10 @@ impl From<InterfaceError> for LibWalletError {
code: 6,
message: format!("{:?}", v),
},
InterfaceError::InvalidArgument(_) => Self {
code: 7,
message: format!("{:?}", v),
},
}
}
}
Expand Down
153 changes: 152 additions & 1 deletion base_layer/wallet_ffi/src/lib.rs
Expand Up @@ -217,6 +217,7 @@ pub struct TariContacts(Vec<TariContact>);
pub type TariContact = tari_wallet::contacts_service::storage::database::Contact;
pub type TariCompletedTransaction = tari_wallet::transaction_service::storage::models::CompletedTransaction;
pub type TariBalance = tari_wallet::output_manager_service::service::Balance;
pub type TariMnemonicLanguage = tari_key_manager::mnemonic::MnemonicLanguage;

pub struct TariCompletedTransactions(Vec<TariCompletedTransaction>);

Expand Down Expand Up @@ -844,6 +845,7 @@ pub unsafe extern "C" fn private_key_from_hex(key: *const c_char, error_out: *mu
}

/// -------------------------------------------------------------------------------------------- ///

/// ----------------------------------- Seed Words ----------------------------------------------///

/// Create an empty instance of TariSeedWords
Expand All @@ -861,6 +863,77 @@ pub unsafe extern "C" fn seed_words_create() -> *mut TariSeedWords {
Box::into_raw(Box::new(TariSeedWords(Vec::new())))
}

/// Create a TariSeedWords instance containing the entire mnemonic wordlist for the requested language
///
/// ## Arguments
/// `language` - The required language as a string
/// `error_out` - Pointer to an int which will be modified to an error code should one occur, may not be null. Functions
/// as an out parameter.
///
/// ## Returns
/// `TariSeedWords` - Returns the TariSeedWords instance containing the entire mnemonic wordlist for the
/// requested language.
///
/// # Safety
/// The `seed_words_destroy` method must be called when finished with a TariSeedWords instance from rust to prevent a
/// memory leak
#[no_mangle]
pub unsafe extern "C" fn seed_words_get_mnemonic_word_list_for_language(
language: *const c_char,
error_out: *mut c_int,
) -> *mut TariSeedWords {
use tari_key_manager::mnemonic_wordlists;

let mut error = 0;
ptr::swap(error_out, &mut error as *mut c_int);

let mut mnemonic_word_list_vec = Vec::new();
if language.is_null() {
error = LibWalletError::from(InterfaceError::NullError("mnemonic wordlist".to_string())).code;
ptr::swap(error_out, &mut error as *mut c_int);
} else {
let not_supported;
let language_string = match CStr::from_ptr(language).to_str() {
Ok(str) => str,
Err(e) => {
not_supported = e.to_string();
not_supported.as_str()
},
};
let mnemonic_word_list = match TariMnemonicLanguage::from_str(language_string) {
Ok(language) => match language {
TariMnemonicLanguage::ChineseSimplified => mnemonic_wordlists::MNEMONIC_CHINESE_SIMPLIFIED_WORDS,
TariMnemonicLanguage::English => mnemonic_wordlists::MNEMONIC_ENGLISH_WORDS,
TariMnemonicLanguage::French => mnemonic_wordlists::MNEMONIC_FRENCH_WORDS,
TariMnemonicLanguage::Italian => mnemonic_wordlists::MNEMONIC_ITALIAN_WORDS,
TariMnemonicLanguage::Japanese => mnemonic_wordlists::MNEMONIC_JAPANESE_WORDS,
TariMnemonicLanguage::Korean => mnemonic_wordlists::MNEMONIC_KOREAN_WORDS,
TariMnemonicLanguage::Spanish => mnemonic_wordlists::MNEMONIC_SPANISH_WORDS,
},
Err(_) => {
error!(
target: LOG_TARGET,
"Mnemonic wordlist - '{}' language not supported", language_string
);
error = LibWalletError::from(InterfaceError::InvalidArgument(format!(
"mnemonic wordlist - '{}' language not supported",
language_string
)))
.code;
ptr::swap(error_out, &mut error as *mut c_int);
[""; 2048]
},
};
info!(
target: LOG_TARGET,
"Retrieved mnemonic wordlist for'{}'", language_string
);
mnemonic_word_list_vec = mnemonic_word_list.to_vec().iter().map(|s| s.to_string()).collect();
}

Box::into_raw(Box::new(TariSeedWords(mnemonic_word_list_vec)))
}

/// Gets the length of TariSeedWords
///
/// ## Arguments
Expand Down Expand Up @@ -915,7 +988,7 @@ pub unsafe extern "C" fn seed_words_get_at(
ptr::swap(error_out, &mut error as *mut c_int);
} else {
let len = (*seed_words).0.len();
if position > len as u32 {
if position >= len as u32 {
error = LibWalletError::from(InterfaceError::PositionInvalidError).code;
ptr::swap(error_out, &mut error as *mut c_int);
} else {
Expand Down Expand Up @@ -967,6 +1040,27 @@ pub unsafe extern "C" fn seed_words_push_word(
}

// Check word is from a word list
match MnemonicLanguage::from(word_string.as_str()) {
Ok(language) => {
if (*seed_words).0.len() >= MnemonicLanguage::word_count(&language) {
let error_msg = "Invalid seed words object, i.e. the entire mnemonic word list, is being used";
log::error!(target: LOG_TARGET, "{}", error_msg);
error = LibWalletError::from(InterfaceError::InvalidArgument(error_msg.to_string())).code;
ptr::swap(error_out, &mut error as *mut c_int);
return SeedWordPushResult::InvalidObject as u8;
}
},
Err(e) => {
log::error!(
target: LOG_TARGET,
"{} is not a valid mnemonic seed word ({:?})",
word_string,
e
);
return SeedWordPushResult::InvalidSeedWord as u8;
},
}

if MnemonicLanguage::from(word_string.as_str()).is_err() {
log::error!(target: LOG_TARGET, "{} is not a valid mnemonic seed word", word_string);
return SeedWordPushResult::InvalidSeedWord as u8;
Expand Down Expand Up @@ -5288,6 +5382,7 @@ mod test {
use tempfile::tempdir;

use tari_common_types::{emoji, transaction::TransactionStatus};
use tari_key_manager::{mnemonic::MnemonicLanguage, mnemonic_wordlists};
use tari_test_utils::random;
use tari_wallet::storage::sqlite_utilities::run_migration_and_create_sqlite_connection;

Expand Down Expand Up @@ -6218,6 +6313,62 @@ mod test {
}
}

#[test]
pub fn test_mnemonic_word_lists() {
unsafe {
let mut error = 0;
let error_ptr = &mut error as *mut c_int;

for language in MnemonicLanguage::iterator() {
let language_str: *const c_char =
CString::into_raw(CString::new(language.to_string()).unwrap()) as *const c_char;
let mnemonic_wordlist_ffi = seed_words_get_mnemonic_word_list_for_language(language_str, error_ptr);
assert_eq!(error, 0);
let mnemonic_wordlist = match *(language) {
TariMnemonicLanguage::ChineseSimplified => mnemonic_wordlists::MNEMONIC_CHINESE_SIMPLIFIED_WORDS,
TariMnemonicLanguage::English => mnemonic_wordlists::MNEMONIC_ENGLISH_WORDS,
TariMnemonicLanguage::French => mnemonic_wordlists::MNEMONIC_FRENCH_WORDS,
TariMnemonicLanguage::Italian => mnemonic_wordlists::MNEMONIC_ITALIAN_WORDS,
TariMnemonicLanguage::Japanese => mnemonic_wordlists::MNEMONIC_JAPANESE_WORDS,
TariMnemonicLanguage::Korean => mnemonic_wordlists::MNEMONIC_KOREAN_WORDS,
TariMnemonicLanguage::Spanish => mnemonic_wordlists::MNEMONIC_SPANISH_WORDS,
};
// Compare from Rust's perspective
assert_eq!(
(*mnemonic_wordlist_ffi).0,
mnemonic_wordlist
.to_vec()
.iter()
.map(|s| s.to_string())
.collect::<Vec<String>>()
);
// Compare from C's perspective
let count = seed_words_get_length(mnemonic_wordlist_ffi, error_ptr);
assert_eq!(error, 0);
for i in 0..count {
// Compare each word in the list
let mnemonic_word_ffi = CString::from_raw(seed_words_get_at(mnemonic_wordlist_ffi, i, error_ptr));
assert_eq!(error, 0);
assert_eq!(
mnemonic_word_ffi.to_str().unwrap().to_string(),
mnemonic_wordlist[i as usize].to_string()
);
}
// Try to wrongfully add a new seed word onto the mnemonic wordlist seed words object
let w = CString::new(mnemonic_wordlist[188]).unwrap();
let w_str: *const c_char = CString::into_raw(w) as *const c_char;
seed_words_push_word(mnemonic_wordlist_ffi, w_str, error_ptr);
assert_eq!(
seed_words_push_word(mnemonic_wordlist_ffi, w_str, error_ptr),
SeedWordPushResult::InvalidObject as u8
);
assert_ne!(error, 0);
// Clear memory
seed_words_destroy(mnemonic_wordlist_ffi);
}
}
}

#[test]
pub fn test_seed_words() {
unsafe {
Expand Down
3 changes: 3 additions & 0 deletions base_layer/wallet_ffi/wallet.h
Expand Up @@ -163,6 +163,9 @@ void private_key_destroy(struct TariPrivateKey *pk);
// Create an empty instance of TariSeedWords
struct TariSeedWords *seed_words_create();

// Create a TariSeedWords instance containing the entire mnemonic wordlist for the requested language
struct TariSeedWords *seed_words_get_mnemonic_word_list_for_language(const char *language, int *error_out);

// Get the number of seed words in the provided collection
unsigned int seed_words_get_length(struct TariSeedWords *seed_words, int *error_out);

Expand Down

0 comments on commit d8e0ced

Please sign in to comment.