Merge pull request #176

b94b8cd Added unit test. Fails for Japanese for some reason. (Oran Juice)
4c8a628 Remove iostream header put in during testing (Oran Juice)
9875f5b Variable unique prefix lengths for seed (Oran Juice)
This commit is contained in:
Riccardo Spagni 2014-10-15 12:14:44 +02:00
commit db5ca73fdd
No known key found for this signature in database
GPG Key ID: 55432DF31CCD4FCD
9 changed files with 215 additions and 50 deletions

View File

@ -61,7 +61,6 @@
namespace namespace
{ {
const int seed_length = 24;
/*! /*!
* \brief Finds the word list that contains the seed words and puts the indices * \brief Finds the word list that contains the seed words and puts the indices
@ -69,11 +68,11 @@ namespace
* \param seed List of words to match. * \param seed List of words to match.
* \param has_checksum If word list passed checksum test, we need to only do a prefix check. * \param has_checksum If word list passed checksum test, we need to only do a prefix check.
* \param matched_indices The indices where the seed words were found are added to this. * \param matched_indices The indices where the seed words were found are added to this.
* \param language Language instance pointer to write to after it is found.
* \return true if all the words were present in some language false if not. * \return true if all the words were present in some language false if not.
*/ */
bool find_seed_language(const std::vector<std::string> &seed, bool find_seed_language(const std::vector<std::string> &seed,
bool has_checksum, std::vector<uint32_t> &matched_indices, uint32_t &word_list_length, bool has_checksum, std::vector<uint32_t> &matched_indices, Language::Base **language)
std::string &language_name)
{ {
// If there's a new language added, add an instance of it here. // If there's a new language added, add an instance of it here.
std::vector<Language::Base*> language_instances({ std::vector<Language::Base*> language_instances({
@ -83,18 +82,6 @@ namespace
Language::Singleton<Language::Japanese>::instance(), Language::Singleton<Language::Japanese>::instance(),
Language::Singleton<Language::OldEnglish>::instance() Language::Singleton<Language::OldEnglish>::instance()
}); });
// To hold trimmed seed words in case of a checksum being present.
std::vector<std::string> trimmed_seed;
if (has_checksum)
{
// If it had a checksum, we'll just compare the unique prefix
// So we create a list of trimmed seed words
for (std::vector<std::string>::const_iterator it = seed.begin(); it != seed.end(); it++)
{
trimmed_seed.push_back(it->length() > Language::unique_prefix_length ?
it->substr(0, Language::unique_prefix_length) : *it);
}
}
// Iterate through all the languages and find a match // Iterate through all the languages and find a match
for (std::vector<Language::Base*>::iterator it1 = language_instances.begin(); for (std::vector<Language::Base*>::iterator it1 = language_instances.begin();
@ -104,23 +91,22 @@ namespace
const std::unordered_map<std::string, uint32_t> &trimmed_word_map = (*it1)->get_trimmed_word_map(); const std::unordered_map<std::string, uint32_t> &trimmed_word_map = (*it1)->get_trimmed_word_map();
// To iterate through seed words // To iterate through seed words
std::vector<std::string>::const_iterator it2; std::vector<std::string>::const_iterator it2;
// To iterate through trimmed seed words
std::vector<std::string>::iterator it3;
bool full_match = true; bool full_match = true;
std::string trimmed_word;
// Iterate through all the words and see if they're all present // Iterate through all the words and see if they're all present
for (it2 = seed.begin(), it3 = trimmed_seed.begin(); for (it2 = seed.begin(); it2 != seed.end(); it2++)
it2 != seed.end(); it2++, it3++)
{ {
if (has_checksum) if (has_checksum)
{ {
trimmed_word = it2->substr(0, (*it1)->get_unique_prefix_length());
// Use the trimmed words and map // Use the trimmed words and map
if (trimmed_word_map.count(*it3) == 0) if (trimmed_word_map.count(trimmed_word) == 0)
{ {
full_match = false; full_match = false;
break; break;
} }
matched_indices.push_back(trimmed_word_map.at(*it3)); matched_indices.push_back(trimmed_word_map.at(trimmed_word));
} }
else else
{ {
@ -134,8 +120,7 @@ namespace
} }
if (full_match) if (full_match)
{ {
word_list_length = (*it1)->get_word_list().size(); *language = *it1;
language_name = (*it1)->get_language_name();
return true; return true;
} }
// Some didn't match. Clear the index array. // Some didn't match. Clear the index array.
@ -146,18 +131,20 @@ namespace
/*! /*!
* \brief Creates a checksum index in the word list array on the list of words. * \brief Creates a checksum index in the word list array on the list of words.
* \param word_list Vector of words * \param word_list Vector of words
* \return Checksum index * \param unique_prefix_length the prefix length of each word to use for checksum
* \return Checksum index
*/ */
uint32_t create_checksum_index(const std::vector<std::string> &word_list) uint32_t create_checksum_index(const std::vector<std::string> &word_list,
uint32_t unique_prefix_length)
{ {
std::string trimmed_words = ""; std::string trimmed_words = "";
for (std::vector<std::string>::const_iterator it = word_list.begin(); it != word_list.end(); it++) for (std::vector<std::string>::const_iterator it = word_list.begin(); it != word_list.end(); it++)
{ {
if (it->length() > 4) if (it->length() > unique_prefix_length)
{ {
trimmed_words += it->substr(0, Language::unique_prefix_length); trimmed_words += it->substr(0, unique_prefix_length);
} }
else else
{ {
@ -166,25 +153,26 @@ namespace
} }
boost::crc_32_type result; boost::crc_32_type result;
result.process_bytes(trimmed_words.data(), trimmed_words.length()); result.process_bytes(trimmed_words.data(), trimmed_words.length());
return result.checksum() % seed_length; return result.checksum() % crypto::ElectrumWords::seed_length;
} }
/*! /*!
* \brief Does the checksum test on the seed passed. * \brief Does the checksum test on the seed passed.
* \param seed Vector of seed words * \param seed Vector of seed words
* \return True if the test passed false if not. * \param unique_prefix_length the prefix length of each word to use for checksum
* \return True if the test passed false if not.
*/ */
bool checksum_test(std::vector<std::string> seed) bool checksum_test(std::vector<std::string> seed, uint32_t unique_prefix_length)
{ {
// The last word is the checksum. // The last word is the checksum.
std::string last_word = seed.back(); std::string last_word = seed.back();
seed.pop_back(); seed.pop_back();
std::string checksum = seed[create_checksum_index(seed)]; std::string checksum = seed[create_checksum_index(seed, unique_prefix_length)];
std::string trimmed_checksum = checksum.length() > 4 ? checksum.substr(0, Language::unique_prefix_length) : std::string trimmed_checksum = checksum.length() > unique_prefix_length ? checksum.substr(0, unique_prefix_length) :
checksum; checksum;
std::string trimmed_last_word = checksum.length() > 4 ? last_word.substr(0, Language::unique_prefix_length) : std::string trimmed_last_word = last_word.length() > unique_prefix_length ? last_word.substr(0, unique_prefix_length) :
last_word; last_word;
return trimmed_checksum == trimmed_last_word; return trimmed_checksum == trimmed_last_word;
} }
@ -211,11 +199,12 @@ namespace crypto
* \param language_name Language of the seed as found gets written here. * \param language_name Language of the seed as found gets written here.
* \return false if not a multiple of 3 words, or if word is not in the words list * \return false if not a multiple of 3 words, or if word is not in the words list
*/ */
bool words_to_bytes(const std::string& words, crypto::secret_key& dst, bool words_to_bytes(std::string words, crypto::secret_key& dst,
std::string &language_name) std::string &language_name)
{ {
std::vector<std::string> seed; std::vector<std::string> seed;
boost::algorithm::trim(words);
boost::split(seed, words, boost::is_any_of(" ")); boost::split(seed, words, boost::is_any_of(" "));
// error on non-compliant word list // error on non-compliant word list
@ -227,22 +216,25 @@ namespace crypto
// If it is seed with a checksum. // If it is seed with a checksum.
bool has_checksum = seed.size() == (seed_length + 1); bool has_checksum = seed.size() == (seed_length + 1);
std::vector<uint32_t> matched_indices;
Language::Base *language;
if (!find_seed_language(seed, has_checksum, matched_indices, &language))
{
return false;
}
language_name = language->get_language_name();
uint32_t word_list_length = language->get_word_list().size();
if (has_checksum) if (has_checksum)
{ {
if (!checksum_test(seed)) if (!checksum_test(seed, language->get_unique_prefix_length()))
{ {
// Checksum fail // Checksum fail
return false; return false;
} }
seed.pop_back(); seed.pop_back();
} }
std::vector<uint32_t> matched_indices;
uint32_t word_list_length = 0;
if (!find_seed_language(seed, has_checksum, matched_indices, word_list_length, language_name))
{
return false;
}
for (unsigned int i=0; i < seed.size() / 3; i++) for (unsigned int i=0; i < seed.size() / 3; i++)
{ {
@ -335,7 +327,7 @@ namespace crypto
} }
words.pop_back(); words.pop_back();
words += (' ' + words_store[create_checksum_index(words_store)]); words += (' ' + words_store[create_checksum_index(words_store, language->get_unique_prefix_length())]);
return false; return false;
} }

View File

@ -59,6 +59,7 @@ namespace crypto
namespace ElectrumWords namespace ElectrumWords
{ {
const int seed_length = 24;
const std::string old_language_name = "OldEnglish"; const std::string old_language_name = "OldEnglish";
/*! /*!
* \brief Converts seed words to bytes (secret key). * \brief Converts seed words to bytes (secret key).
@ -67,7 +68,7 @@ namespace crypto
* \param language_name Language of the seed as found gets written here. * \param language_name Language of the seed as found gets written here.
* \return false if not a multiple of 3 words, or if word is not in the words list * \return false if not a multiple of 3 words, or if word is not in the words list
*/ */
bool words_to_bytes(const std::string& words, crypto::secret_key& dst, bool words_to_bytes(std::string words, crypto::secret_key& dst,
std::string &language_name); std::string &language_name);
/*! /*!

View File

@ -1681,6 +1681,7 @@ namespace Language
"zones", "zones",
"zoom" "zoom"
}); });
unique_prefix_length = 3;
word_map = new std::unordered_map<std::string, uint32_t>; word_map = new std::unordered_map<std::string, uint32_t>;
trimmed_word_map = new std::unordered_map<std::string, uint32_t>; trimmed_word_map = new std::unordered_map<std::string, uint32_t>;
language_name = "English"; language_name = "English";

View File

@ -1681,6 +1681,7 @@ namespace Language
"びじゅつかん", "びじゅつかん",
"ひしょ" "ひしょ"
}); });
unique_prefix_length = 4;
word_map = new std::unordered_map<std::string, uint32_t>; word_map = new std::unordered_map<std::string, uint32_t>;
trimmed_word_map = new std::unordered_map<std::string, uint32_t>; trimmed_word_map = new std::unordered_map<std::string, uint32_t>;
language_name = "Japanese"; language_name = "Japanese";

View File

@ -45,7 +45,6 @@
*/ */
namespace Language namespace Language
{ {
const int unique_prefix_length = 4; /*!< Length of the prefix of all words guaranteed to be unique */
/*! /*!
* \class Base * \class Base
* \brief A base language class which all languages have to inherit from for * \brief A base language class which all languages have to inherit from for
@ -58,7 +57,7 @@ namespace Language
std::unordered_map<std::string, uint32_t> *word_map; /*!< hash table to find word's index */ std::unordered_map<std::string, uint32_t> *word_map; /*!< hash table to find word's index */
std::unordered_map<std::string, uint32_t> *trimmed_word_map; /*!< hash table to find word's trimmed index */ std::unordered_map<std::string, uint32_t> *trimmed_word_map; /*!< hash table to find word's trimmed index */
std::string language_name; /*!< Name of language */ std::string language_name; /*!< Name of language */
int trim_length; /*!< Number of unique starting characters to trim the wordlist to when matching */ uint32_t unique_prefix_length; /*!< Number of unique starting characters to trim the wordlist to when matching */
/*! /*!
* \brief Populates the word maps after the list is ready. * \brief Populates the word maps after the list is ready.
*/ */
@ -85,6 +84,7 @@ namespace Language
word_list = new std::vector<std::string>; word_list = new std::vector<std::string>;
word_map = new std::unordered_map<std::string, uint32_t>; word_map = new std::unordered_map<std::string, uint32_t>;
trimmed_word_map = new std::unordered_map<std::string, uint32_t>; trimmed_word_map = new std::unordered_map<std::string, uint32_t>;
unique_prefix_length = 4;
} }
/*! /*!
* \brief Returns a pointer to the word list. * \brief Returns a pointer to the word list.
@ -122,9 +122,9 @@ namespace Language
* \brief Returns the number of unique starting characters to be used for matching. * \brief Returns the number of unique starting characters to be used for matching.
* \return Number of unique starting characters. * \return Number of unique starting characters.
*/ */
int get_trim_length() const uint32_t get_unique_prefix_length() const
{ {
return trim_length; return unique_prefix_length;
} }
}; };
} }

View File

@ -1681,6 +1681,7 @@ namespace Language
"weapon", "weapon",
"weary" "weary"
}); });
unique_prefix_length = 4;
word_map = new std::unordered_map<std::string, uint32_t>; word_map = new std::unordered_map<std::string, uint32_t>;
trimmed_word_map = new std::unordered_map<std::string, uint32_t>; trimmed_word_map = new std::unordered_map<std::string, uint32_t>;
language_name = "OldEnglish"; language_name = "OldEnglish";

View File

@ -1679,6 +1679,7 @@ namespace Language
"zenite", "zenite",
"zumbi" "zumbi"
}); });
unique_prefix_length = 4;
word_map = new std::unordered_map<std::string, uint32_t>; word_map = new std::unordered_map<std::string, uint32_t>;
trimmed_word_map = new std::unordered_map<std::string, uint32_t>; trimmed_word_map = new std::unordered_map<std::string, uint32_t>;
language_name = "Portuguese"; language_name = "Portuguese";

View File

@ -1681,6 +1681,7 @@ namespace Language
"ritmo", "ritmo",
"rito" "rito"
}); });
unique_prefix_length = 4;
word_map = new std::unordered_map<std::string, uint32_t>; word_map = new std::unordered_map<std::string, uint32_t>;
trimmed_word_map = new std::unordered_map<std::string, uint32_t>; trimmed_word_map = new std::unordered_map<std::string, uint32_t>;
language_name = "Spanish"; language_name = "Spanish";

View File

@ -0,0 +1,167 @@
// Copyright (c) 2014, The Monero Project
//
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without modification, are
// permitted provided that the following conditions are met:
//
// 1. Redistributions of source code must retain the above copyright notice, this list of
// conditions and the following disclaimer.
//
// 2. Redistributions in binary form must reproduce the above copyright notice, this list
// of conditions and the following disclaimer in the documentation and/or other
// materials provided with the distribution.
//
// 3. Neither the name of the copyright holder nor the names of its contributors may be
// used to endorse or promote products derived from this software without specific
// prior written permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY
// EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
// MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL
// THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
// PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
// INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF
// THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#include "gtest/gtest.h"
#include "mnemonics/electrum-words.h"
#include "crypto/crypto.h"
#include <stdlib.h>
#include <vector>
#include <time.h>
#include <iostream>
#include <boost/algorithm/string.hpp>
#include "mnemonics/english.h"
#include "mnemonics/spanish.h"
#include "mnemonics/portuguese.h"
#include "mnemonics/japanese.h"
#include "mnemonics/old_english.h"
#include "mnemonics/language_base.h"
#include "mnemonics/singleton.h"
namespace
{
/*!
* \brief Returns random index from 0 to max-1
* \param max Range maximum
* \return required random index
*/
uint32_t get_random_index(int max)
{
return rand() % max;
}
/*!
* \brief Print a seed
* \param seed word list
*/
void print_seed(const std::vector<std::string> &seed)
{
for (std::vector<std::string>::const_iterator it = seed.begin(); it != seed.end(); it++)
{
std::cout << *it << " ";
}
std::cout << std::endl;
}
/*!
* \brief Compares vectors for equality
* \param expected expected vector
* \param present current vector
*/
void compare_vectors(const std::vector<std::string> &expected, const std::vector<std::string> &present)
{
std::vector<std::string>::const_iterator it1, it2;
for (it1 = expected.begin(), it2 = present.begin(); it1 != expected.end() && it2 != present.end();
it1++, it2++)
{
ASSERT_STREQ(it1->c_str(), it2->c_str());
}
}
/*!
* \brief Tests the given language mnemonics.
* \param language A Language instance to test
*/
void test_language(const Language::Base &language)
{
const std::vector<std::string> &word_list = language.get_word_list();
std::string seed = "", return_seed = "";
// Generate a random seed without checksum
for (int ii = 0; ii < crypto::ElectrumWords::seed_length; ii++)
{
seed += (word_list[get_random_index(word_list.size())] + ' ');
}
seed.pop_back();
std::cout << "Test seed without checksum:\n";
std::cout << seed << std::endl;
crypto::secret_key key;
std::string language_name;
bool res;
std::vector<std::string> seed_vector, return_seed_vector;
std::string checksum_word;
// Convert it to secret key
res = crypto::ElectrumWords::words_to_bytes(seed, key, language_name);
ASSERT_EQ(true, res);
std::cout << "Detected language: " << language_name << std::endl;
ASSERT_STREQ(language.get_language_name().c_str(), language_name.c_str());
// Convert the secret key back to seed
crypto::ElectrumWords::bytes_to_words(key, return_seed, language.get_language_name());
ASSERT_EQ(true, res);
std::cout << "Returned seed:\n";
std::cout << return_seed << std::endl;
boost::split(seed_vector, seed, boost::is_any_of(" "));
boost::split(return_seed_vector, return_seed, boost::is_any_of(" "));
// Extract the checksum word
checksum_word = return_seed_vector.back();
return_seed_vector.pop_back();
ASSERT_EQ(seed_vector.size(), return_seed_vector.size());
// Ensure that the rest of it is same
compare_vectors(seed_vector, return_seed_vector);
// Append the checksum word to repeat the entire process with a seed with checksum
seed += (" " + checksum_word);
std::cout << "Test seed with checksum:\n";
std::cout << seed << std::endl;
res = crypto::ElectrumWords::words_to_bytes(seed, key, language_name);
ASSERT_EQ(true, res);
std::cout << "Detected language: " << language_name << std::endl;
ASSERT_STREQ(language.get_language_name().c_str(), language_name.c_str());
return_seed = "";
crypto::ElectrumWords::bytes_to_words(key, return_seed, language.get_language_name());
ASSERT_EQ(true, res);
std::cout << "Returned seed:\n";
std::cout << return_seed << std::endl;
seed_vector.clear();
return_seed_vector.clear();
boost::split(seed_vector, seed, boost::is_any_of(" "));
boost::split(return_seed_vector, return_seed, boost::is_any_of(" "));
ASSERT_EQ(seed_vector.size(), return_seed_vector.size());
compare_vectors(seed_vector, return_seed_vector);
}
}
TEST(mnemonics, all_languages)
{
srand(time(NULL));
std::vector<Language::Base*> languages({
Language::Singleton<Language::English>::instance(),
Language::Singleton<Language::Spanish>::instance(),
Language::Singleton<Language::Portuguese>::instance(),
Language::Singleton<Language::Japanese>::instance(),
});
for (std::vector<Language::Base*>::iterator it = languages.begin(); it != languages.end(); it++)
{
test_language(*(*it));
}
}