From ea37614efe518ff8f363ddf2465301687e04d977 Mon Sep 17 00:00:00 2001 From: moneromooo-monero Date: Sat, 7 Jul 2018 00:03:15 +0100 Subject: [PATCH] wallet: wipe seed from memory where appropriate --- contrib/epee/include/fnv1.h | 45 ++++++ contrib/epee/include/hex.h | 5 + contrib/epee/include/wipeable_string.h | 28 +++- contrib/epee/src/hex.cpp | 10 +- contrib/epee/src/wipeable_string.cpp | 112 +++++++++++++- src/common/password.cpp | 22 +-- src/common/password.h | 2 +- src/mnemonics/electrum-words.cpp | 120 ++++++++------- src/mnemonics/electrum-words.h | 12 +- src/mnemonics/language_base.h | 29 ++-- src/simplewallet/simplewallet.cpp | 105 ++++++++++--- src/simplewallet/simplewallet.h | 7 +- src/wallet/api/wallet.cpp | 4 +- src/wallet/wallet2.cpp | 44 ++++-- src/wallet/wallet2.h | 11 +- src/wallet/wallet_rpc_server.cpp | 4 +- tests/unit_tests/CMakeLists.txt | 3 +- tests/unit_tests/mnemonics.cpp | 30 +++- tests/unit_tests/wipeable_string.cpp | 204 +++++++++++++++++++++++++ 19 files changed, 653 insertions(+), 144 deletions(-) create mode 100644 contrib/epee/include/fnv1.h create mode 100644 tests/unit_tests/wipeable_string.cpp diff --git a/contrib/epee/include/fnv1.h b/contrib/epee/include/fnv1.h new file mode 100644 index 000000000..c04389bca --- /dev/null +++ b/contrib/epee/include/fnv1.h @@ -0,0 +1,45 @@ +// Copyright (c) 2018, 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. + +#pragma once + +namespace epee +{ + +namespace fnv +{ + inline uint64_t FNV1a(const char *ptr, size_t sz) + { + uint64_t h = 0xcbf29ce484222325; + for (size_t i = 0; i < sz; ++i) + h = (h ^ *(const uint8_t*)ptr++) * 0x100000001b3; + return h; + } +} + +} diff --git a/contrib/epee/include/hex.h b/contrib/epee/include/hex.h index e960da1d2..02600c320 100644 --- a/contrib/epee/include/hex.h +++ b/contrib/epee/include/hex.h @@ -33,6 +33,7 @@ #include #include +#include "wipeable_string.h" #include "span.h" namespace epee @@ -41,6 +42,8 @@ namespace epee { //! \return A std::string containing hex of `src`. static std::string string(const span src); + //! \return A epee::wipeable_string containing hex of `src`. + static epee::wipeable_string wipeable_string(const span src); //! \return An array containing hex of `src`. template @@ -59,6 +62,8 @@ namespace epee static void formatted(std::ostream& out, const span src); private: + template T static convert(const span src); + //! Write `src` bytes as hex to `out`. `out` must be twice the length static void buffer_unchecked(char* out, const span src) noexcept; }; diff --git a/contrib/epee/include/wipeable_string.h b/contrib/epee/include/wipeable_string.h index 70d1a9586..4cebe5fdf 100644 --- a/contrib/epee/include/wipeable_string.h +++ b/contrib/epee/include/wipeable_string.h @@ -28,28 +28,43 @@ #pragma once +#include #include #include #include +#include "fnv1.h" namespace epee { class wipeable_string { public: + typedef char value_type; + wipeable_string() {} wipeable_string(const wipeable_string &other); wipeable_string(wipeable_string &&other); wipeable_string(const std::string &other); wipeable_string(std::string &&other); wipeable_string(const char *s); + wipeable_string(const char *s, size_t len); ~wipeable_string(); void wipe(); void push_back(char c); - void pop_back(); + void operator+=(char c); + void operator+=(const std::string &s); + void operator+=(const epee::wipeable_string &s); + void operator+=(const char *s); + void append(const char *ptr, size_t len); + char pop_back(); const char *data() const noexcept { return buffer.data(); } + char *data() noexcept { return buffer.data(); } size_t size() const noexcept { return buffer.size(); } + size_t length() const noexcept { return buffer.size(); } bool empty() const noexcept { return buffer.empty(); } + void trim(); + void split(std::vector &fields) const; + boost::optional parse_hexstr() const; void resize(size_t sz); void reserve(size_t sz); void clear(); @@ -65,3 +80,14 @@ namespace epee std::vector buffer; }; } + +namespace std +{ + template<> struct hash + { + size_t operator()(const epee::wipeable_string &s) const + { + return epee::fnv::FNV1a(s.data(), s.size()); + } + }; +} diff --git a/contrib/epee/src/hex.cpp b/contrib/epee/src/hex.cpp index c143b2dc2..5c8acc8be 100644 --- a/contrib/epee/src/hex.cpp +++ b/contrib/epee/src/hex.cpp @@ -52,17 +52,21 @@ namespace epee } } - std::string to_hex::string(const span src) + template + T to_hex::convert(const span src) { if (std::numeric_limits::max() / 2 < src.size()) throw std::range_error("hex_view::to_string exceeded maximum size"); - std::string out{}; + T out{}; out.resize(src.size() * 2); - buffer_unchecked(std::addressof(out[0]), src); + to_hex::buffer_unchecked((char*)out.data(), src); // can't see the non const version in wipeable_string?? return out; } + std::string to_hex::string(const span src) { return convert(src); } + epee::wipeable_string to_hex::wipeable_string(const span src) { return convert(src); } + void to_hex::buffer(std::ostream& out, const span src) { write_hex(std::ostreambuf_iterator{out}, src); diff --git a/contrib/epee/src/wipeable_string.cpp b/contrib/epee/src/wipeable_string.cpp index 6ed4ee8a2..7c9722765 100644 --- a/contrib/epee/src/wipeable_string.cpp +++ b/contrib/epee/src/wipeable_string.cpp @@ -26,11 +26,22 @@ // 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 #include #include "memwipe.h" #include "misc_log_ex.h" #include "wipeable_string.h" +namespace +{ + int atolower(int c) + { + if (c >= 'A' && c <= 'Z') + c |= 32; + return c; + } +} + namespace epee { @@ -69,6 +80,12 @@ wipeable_string::wipeable_string(const char *s) memcpy(buffer.data(), s, size()); } +wipeable_string::wipeable_string(const char *s, size_t len) +{ + grow(len); + memcpy(buffer.data(), s, len); +} + wipeable_string::~wipeable_string() { wipe(); @@ -109,9 +126,100 @@ void wipeable_string::push_back(char c) buffer.back() = c; } -void wipeable_string::pop_back() +void wipeable_string::operator+=(char c) { - resize(size() - 1); + push_back(c); +} + +void wipeable_string::append(const char *ptr, size_t len) +{ + const size_t orgsz = size(); + CHECK_AND_ASSERT_THROW_MES(orgsz < std::numeric_limits::max() - len, "Appended data too large"); + grow(orgsz + len); + if (len > 0) + memcpy(data() + orgsz, ptr, len); +} + +void wipeable_string::operator+=(const char *s) +{ + append(s, strlen(s)); +} + +void wipeable_string::operator+=(const epee::wipeable_string &s) +{ + append(s.data(), s.size()); +} + +void wipeable_string::operator+=(const std::string &s) +{ + append(s.c_str(), s.size()); +} + +void wipeable_string::trim() +{ + size_t prefix = 0; + while (prefix < size() && data()[prefix] == ' ') + ++prefix; + if (prefix > 0) + memmove(buffer.data(), buffer.data() + prefix, size() - prefix); + + size_t suffix = 0; + while (suffix < size()-prefix && data()[size() - 1 - prefix - suffix] == ' ') + ++suffix; + + resize(size() - prefix - suffix); +} + +void wipeable_string::split(std::vector &fields) const +{ + fields.clear(); + size_t len = size(); + const char *ptr = data(); + bool space = true; + while (len--) + { + const char c = *ptr++; + if (c != ' ') + { + if (space) + fields.push_back({}); + fields.back().push_back(c); + } + space = c == ' '; + } +} + +boost::optional wipeable_string::parse_hexstr() const +{ + if (size() % 2 != 0) + return boost::none; + boost::optional res = epee::wipeable_string(""); + const size_t len = size(); + const char *d = data(); + res->grow(0, len / 2); + static constexpr const char hex[] = u8"0123456789abcdef"; + for (size_t i = 0; i < len; i += 2) + { + char c = atolower(d[i]); + const char *ptr0 = strchr(hex, c); + if (!ptr0) + return boost::none; + c = atolower(d[i+1]); + const char *ptr1 = strchr(hex, c); + if (!ptr1) + return boost::none; + res->push_back(((ptr0-hex)<<4) | (ptr1-hex)); + } + return res; +} + +char wipeable_string::pop_back() +{ + const size_t sz = size(); + CHECK_AND_ASSERT_THROW_MES(sz > 0, "Popping from an empty string"); + const char c = buffer.back(); + resize(sz - 1); + return c; } void wipeable_string::resize(size_t sz) diff --git a/src/common/password.cpp b/src/common/password.cpp index 3ce2ba42a..5671c4a4e 100644 --- a/src/common/password.cpp +++ b/src/common/password.cpp @@ -54,7 +54,7 @@ namespace return 0 != _isatty(_fileno(stdin)); } - bool read_from_tty(epee::wipeable_string& pass) + bool read_from_tty(epee::wipeable_string& pass, bool hide_input) { static constexpr const char BACKSPACE = 8; @@ -62,7 +62,7 @@ namespace DWORD mode_old; ::GetConsoleMode(h_cin, &mode_old); - DWORD mode_new = mode_old & ~(ENABLE_ECHO_INPUT | ENABLE_LINE_INPUT); + DWORD mode_new = mode_old & ~((hide_input ? ENABLE_ECHO_INPUT : 0) | ENABLE_LINE_INPUT); ::SetConsoleMode(h_cin, mode_new); bool r = true; @@ -107,14 +107,14 @@ namespace return 0 != isatty(fileno(stdin)); } - int getch() noexcept + int getch(bool hide_input) noexcept { struct termios tty_old; tcgetattr(STDIN_FILENO, &tty_old); struct termios tty_new; tty_new = tty_old; - tty_new.c_lflag &= ~(ICANON | ECHO); + tty_new.c_lflag &= ~(ICANON | (hide_input ? ECHO : 0)); tcsetattr(STDIN_FILENO, TCSANOW, &tty_new); int ch = getchar(); @@ -124,14 +124,14 @@ namespace return ch; } - bool read_from_tty(epee::wipeable_string& aPass) + bool read_from_tty(epee::wipeable_string& aPass, bool hide_input) { static constexpr const char BACKSPACE = 127; aPass.reserve(tools::password_container::max_password_size); while (aPass.size() < tools::password_container::max_password_size) { - int ch = getch(); + int ch = getch(hide_input); if (EOF == ch || ch == EOT) { return false; @@ -159,18 +159,18 @@ namespace #endif // end !WIN32 - bool read_from_tty(const bool verify, const char *message, epee::wipeable_string& pass1, epee::wipeable_string& pass2) + bool read_from_tty(const bool verify, const char *message, bool hide_input, epee::wipeable_string& pass1, epee::wipeable_string& pass2) { while (true) { if (message) std::cout << message <<": " << std::flush; - if (!read_from_tty(pass1)) + if (!read_from_tty(pass1, hide_input)) return false; if (verify) { std::cout << "Confirm password: "; - if (!read_from_tty(pass2)) + if (!read_from_tty(pass2, hide_input)) return false; if(pass1!=pass2) { @@ -229,12 +229,12 @@ namespace tools std::atomic password_container::is_prompting(false); - boost::optional password_container::prompt(const bool verify, const char *message) + boost::optional password_container::prompt(const bool verify, const char *message, bool hide_input) { is_prompting = true; password_container pass1{}; password_container pass2{}; - if (is_cin_tty() ? read_from_tty(verify, message, pass1.m_password, pass2.m_password) : read_from_file(pass1.m_password)) + if (is_cin_tty() ? read_from_tty(verify, message, hide_input, pass1.m_password, pass2.m_password) : read_from_file(pass1.m_password)) { is_prompting = false; return {std::move(pass1)}; diff --git a/src/common/password.h b/src/common/password.h index 61937b93a..529881e40 100644 --- a/src/common/password.h +++ b/src/common/password.h @@ -49,7 +49,7 @@ namespace tools password_container(std::string&& password) noexcept; //! \return A password from stdin TTY prompt or `std::cin` pipe. - static boost::optional prompt(bool verify, const char *mesage = "Password"); + static boost::optional prompt(bool verify, const char *mesage = "Password", bool hide_input = true); static std::atomic is_prompting; password_container(const password_container&) = delete; diff --git a/src/mnemonics/electrum-words.cpp b/src/mnemonics/electrum-words.cpp index 19a9c26bb..290f2cb93 100644 --- a/src/mnemonics/electrum-words.cpp +++ b/src/mnemonics/electrum-words.cpp @@ -43,6 +43,8 @@ #include #include #include +#include "wipeable_string.h" +#include "misc_language.h" #include "crypto/crypto.h" // for declaration of crypto::secret_key #include #include "mnemonics/electrum-words.h" @@ -80,9 +82,9 @@ namespace crypto namespace { - uint32_t create_checksum_index(const std::vector &word_list, + uint32_t create_checksum_index(const std::vector &word_list, uint32_t unique_prefix_length); - bool checksum_test(std::vector seed, uint32_t unique_prefix_length); + bool checksum_test(std::vector seed, uint32_t unique_prefix_length); /*! * \brief Finds the word list that contains the seed words and puts the indices @@ -93,7 +95,7 @@ namespace * \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. */ - bool find_seed_language(const std::vector &seed, + bool find_seed_language(const std::vector &seed, bool has_checksum, std::vector &matched_indices, Language::Base **language) { // If there's a new language added, add an instance of it here. @@ -114,17 +116,19 @@ namespace }); Language::Base *fallback = NULL; + std::vector::const_iterator it2; + matched_indices.reserve(seed.size()); + // Iterate through all the languages and find a match for (std::vector::iterator it1 = language_instances.begin(); it1 != language_instances.end(); it1++) { - const std::unordered_map &word_map = (*it1)->get_word_map(); - const std::unordered_map &trimmed_word_map = (*it1)->get_trimmed_word_map(); + const std::unordered_map &word_map = (*it1)->get_word_map(); + const std::unordered_map &trimmed_word_map = (*it1)->get_trimmed_word_map(); // To iterate through seed words - std::vector::const_iterator it2; bool full_match = true; - std::string trimmed_word; + epee::wipeable_string trimmed_word; // Iterate through all the words and see if they're all present for (it2 = seed.begin(); it2 != seed.end(); it2++) { @@ -167,6 +171,7 @@ namespace return true; } // Some didn't match. Clear the index array. + memwipe(matched_indices.data(), matched_indices.size() * sizeof(matched_indices[0])); matched_indices.clear(); } @@ -181,6 +186,7 @@ namespace } MINFO("No match found"); + memwipe(matched_indices.data(), matched_indices.size() * sizeof(matched_indices[0])); return false; } @@ -190,12 +196,12 @@ namespace * \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 &word_list, + uint32_t create_checksum_index(const std::vector &word_list, uint32_t unique_prefix_length) { - std::string trimmed_words = ""; + epee::wipeable_string trimmed_words = ""; - for (std::vector::const_iterator it = word_list.begin(); it != word_list.end(); it++) + for (std::vector::const_iterator it = word_list.begin(); it != word_list.end(); it++) { if (it->length() > unique_prefix_length) { @@ -217,22 +223,22 @@ namespace * \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 seed, uint32_t unique_prefix_length) + bool checksum_test(std::vector seed, uint32_t unique_prefix_length) { if (seed.empty()) return false; // The last word is the checksum. - std::string last_word = seed.back(); + epee::wipeable_string last_word = seed.back(); seed.pop_back(); - std::string checksum = seed[create_checksum_index(seed, unique_prefix_length)]; + epee::wipeable_string checksum = seed[create_checksum_index(seed, unique_prefix_length)]; - std::string trimmed_checksum = checksum.length() > unique_prefix_length ? Language::utf8prefix(checksum, unique_prefix_length) : + epee::wipeable_string trimmed_checksum = checksum.length() > unique_prefix_length ? Language::utf8prefix(checksum, unique_prefix_length) : checksum; - std::string trimmed_last_word = last_word.length() > unique_prefix_length ? Language::utf8prefix(last_word, unique_prefix_length) : + epee::wipeable_string trimmed_last_word = last_word.length() > unique_prefix_length ? Language::utf8prefix(last_word, unique_prefix_length) : last_word; bool ret = trimmed_checksum == trimmed_last_word; - MINFO("Checksum is %s" << (ret ? "valid" : "invalid")); + MINFO("Checksum is " << (ret ? "valid" : "invalid")); return ret; } } @@ -260,13 +266,12 @@ namespace crypto * \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 */ - bool words_to_bytes(std::string words, std::string& dst, size_t len, bool duplicate, + bool words_to_bytes(const epee::wipeable_string &words, epee::wipeable_string& dst, size_t len, bool duplicate, std::string &language_name) { - std::vector seed; + std::vector seed; - boost::algorithm::trim(words); - boost::split(seed, words, boost::is_any_of(" "), boost::token_compress_on); + words.split(seed); if (len % 4) { @@ -291,6 +296,7 @@ namespace crypto } std::vector matched_indices; + auto wiper = epee::misc_utils::create_scope_leave_handler([&](){memwipe(matched_indices.data(), matched_indices.size() * sizeof(matched_indices[0]));}); Language::Base *language; if (!find_seed_language(seed, has_checksum, matched_indices, &language)) { @@ -313,33 +319,33 @@ namespace crypto for (unsigned int i=0; i < seed.size() / 3; i++) { - uint32_t val; - uint32_t w1, w2, w3; - w1 = matched_indices[i*3]; - w2 = matched_indices[i*3 + 1]; - w3 = matched_indices[i*3 + 2]; + uint32_t w[4]; + w[1] = matched_indices[i*3]; + w[2] = matched_indices[i*3 + 1]; + w[3] = matched_indices[i*3 + 2]; - val = w1 + word_list_length * (((word_list_length - w1) + w2) % word_list_length) + - word_list_length * word_list_length * (((word_list_length - w2) + w3) % word_list_length); + w[0]= w[1] + word_list_length * (((word_list_length - w[1]) + w[2]) % word_list_length) + + word_list_length * word_list_length * (((word_list_length - w[2]) + w[3]) % word_list_length); - if (!(val % word_list_length == w1)) + if (!(w[0]% word_list_length == w[1])) { + memwipe(w, sizeof(w)); MERROR("Invalid seed: mumble mumble"); return false; } - dst.append((const char*)&val, 4); // copy 4 bytes to position + dst.append((const char*)&w[0], 4); // copy 4 bytes to position + memwipe(w, sizeof(w)); } if (len > 0 && duplicate) { const size_t expected = len * 3 / 32; - std::string wlist_copy = words; if (seed.size() == expected/2) { - dst.append(dst); // if electrum 12-word seed, duplicate - wlist_copy += ' '; - wlist_copy += words; + dst += ' '; // if electrum 12-word seed, duplicate + dst += dst; // if electrum 12-word seed, duplicate + dst.pop_back(); // trailing space } } @@ -353,10 +359,10 @@ namespace crypto * \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 */ - bool words_to_bytes(std::string words, crypto::secret_key& dst, + bool words_to_bytes(const epee::wipeable_string &words, crypto::secret_key& dst, std::string &language_name) { - std::string s; + epee::wipeable_string s; if (!words_to_bytes(words, s, sizeof(dst), true, language_name)) { MERROR("Invalid seed: failed to convert words to bytes"); @@ -378,7 +384,7 @@ namespace crypto * \param language_name Seed language name * \return true if successful false if not. Unsuccessful if wrong key size. */ - bool bytes_to_words(const char *src, size_t len, std::string& words, + bool bytes_to_words(const char *src, size_t len, epee::wipeable_string& words, const std::string &language_name) { @@ -397,39 +403,38 @@ namespace crypto } const std::vector &word_list = language->get_word_list(); // To store the words for random access to add the checksum word later. - std::vector words_store; + std::vector words_store; uint32_t word_list_length = word_list.size(); // 4 bytes -> 3 words. 8 digits base 16 -> 3 digits base 1626 - for (unsigned int i=0; i < len/4; i++, words += ' ') + for (unsigned int i=0; i < len/4; i++, words.push_back(' ')) { - uint32_t w1, w2, w3; - - uint32_t val; + uint32_t w[4]; - memcpy(&val, src + (i * 4), 4); + memcpy(&w[0], src + (i * 4), 4); - w1 = val % word_list_length; - w2 = ((val / word_list_length) + w1) % word_list_length; - w3 = (((val / word_list_length) / word_list_length) + w2) % word_list_length; + w[1] = w[0] % word_list_length; + w[2] = ((w[0] / word_list_length) + w[1]) % word_list_length; + w[3] = (((w[0] / word_list_length) / word_list_length) + w[2]) % word_list_length; - words += word_list[w1]; + words += word_list[w[1]]; words += ' '; - words += word_list[w2]; + words += word_list[w[2]]; words += ' '; - words += word_list[w3]; + words += word_list[w[3]]; - words_store.push_back(word_list[w1]); - words_store.push_back(word_list[w2]); - words_store.push_back(word_list[w3]); + words_store.push_back(word_list[w[1]]); + words_store.push_back(word_list[w[2]]); + words_store.push_back(word_list[w[3]]); + + memwipe(w, sizeof(w)); } - words.pop_back(); - words += (' ' + words_store[create_checksum_index(words_store, language->get_unique_prefix_length())]); + words += words_store[create_checksum_index(words_store, language->get_unique_prefix_length())]; return true; } - bool bytes_to_words(const crypto::secret_key& src, std::string& words, + bool bytes_to_words(const crypto::secret_key& src, epee::wipeable_string& words, const std::string &language_name) { return bytes_to_words(src.data, sizeof(src), words, language_name); @@ -473,11 +478,10 @@ namespace crypto * \param seed The seed to check (a space delimited concatenated word list) * \return true if the seed passed is a old style seed false if not. */ - bool get_is_old_style_seed(std::string seed) + bool get_is_old_style_seed(const epee::wipeable_string &seed) { - std::vector word_list; - boost::algorithm::trim(seed); - boost::split(word_list, seed, boost::is_any_of(" "), boost::token_compress_on); + std::vector word_list; + seed.split(word_list); return word_list.size() != (seed_length + 1); } diff --git a/src/mnemonics/electrum-words.h b/src/mnemonics/electrum-words.h index 856edb92a..5401b9779 100644 --- a/src/mnemonics/electrum-words.h +++ b/src/mnemonics/electrum-words.h @@ -44,6 +44,8 @@ #include #include "crypto/crypto.h" // for declaration of crypto::secret_key +namespace epee { class wipeable_string; } + /*! * \namespace crypto * @@ -70,7 +72,7 @@ namespace crypto * \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 */ - bool words_to_bytes(std::string words, std::string& dst, size_t len, bool duplicate, + bool words_to_bytes(const epee::wipeable_string &words, epee::wipeable_string& dst, size_t len, bool duplicate, std::string &language_name); /*! * \brief Converts seed words to bytes (secret key). @@ -79,7 +81,7 @@ namespace crypto * \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 */ - bool words_to_bytes(std::string words, crypto::secret_key& dst, + bool words_to_bytes(const epee::wipeable_string &words, crypto::secret_key& dst, std::string &language_name); /*! @@ -90,7 +92,7 @@ namespace crypto * \param language_name Seed language name * \return true if successful false if not. Unsuccessful if wrong key size. */ - bool bytes_to_words(const char *src, size_t len, std::string& words, + bool bytes_to_words(const char *src, size_t len, epee::wipeable_string& words, const std::string &language_name); /*! @@ -100,7 +102,7 @@ namespace crypto * \param language_name Seed language name * \return true if successful false if not. Unsuccessful if wrong key size. */ - bool bytes_to_words(const crypto::secret_key& src, std::string& words, + bool bytes_to_words(const crypto::secret_key& src, epee::wipeable_string& words, const std::string &language_name); /*! @@ -115,7 +117,7 @@ namespace crypto * \param seed The seed to check (a space delimited concatenated word list) * \return true if the seed passed is a old style seed false if not. */ - bool get_is_old_style_seed(std::string seed); + bool get_is_old_style_seed(const epee::wipeable_string &seed); /*! * \brief Returns the name of a language in English diff --git a/src/mnemonics/language_base.h b/src/mnemonics/language_base.h index 2b0c37c6b..cf518ab2a 100644 --- a/src/mnemonics/language_base.h +++ b/src/mnemonics/language_base.h @@ -53,15 +53,20 @@ namespace Language * \param count How many characters to return. * \return A string consisting of the first count characters in s. */ - inline std::string utf8prefix(const std::string &s, size_t count) + template + inline T utf8prefix(const T &s, size_t count) { - std::string prefix = ""; - const char *ptr = s.c_str(); - while (count-- && *ptr) + T prefix = ""; + size_t avail = s.size(); + const char *ptr = s.data(); + while (count-- && avail--) { prefix += *ptr++; - while (((*ptr) & 0xc0) == 0x80) + while (avail && ((*ptr) & 0xc0) == 0x80) + { prefix += *ptr++; + --avail; + } } return prefix; } @@ -79,8 +84,8 @@ namespace Language ALLOW_DUPLICATE_PREFIXES = 1<<1, }; const std::vector word_list; /*!< A pointer to the array of words */ - std::unordered_map word_map; /*!< hash table to find word's index */ - std::unordered_map trimmed_word_map; /*!< hash table to find word's trimmed index */ + std::unordered_map word_map; /*!< hash table to find word's index */ + std::unordered_map trimmed_word_map; /*!< hash table to find word's trimmed index */ std::string language_name; /*!< Name of language */ std::string english_language_name; /*!< Name of language */ uint32_t unique_prefix_length; /*!< Number of unique starting characters to trim the wordlist to when matching */ @@ -103,7 +108,7 @@ namespace Language else throw std::runtime_error("Too short word in " + language_name + " word list: " + *it); } - std::string trimmed; + epee::wipeable_string trimmed; if (it->length() > unique_prefix_length) { trimmed = utf8prefix(*it, unique_prefix_length); @@ -115,9 +120,9 @@ namespace Language if (trimmed_word_map.find(trimmed) != trimmed_word_map.end()) { if (flags & ALLOW_DUPLICATE_PREFIXES) - MWARNING("Duplicate prefix in " << language_name << " word list: " << trimmed); + MWARNING("Duplicate prefix in " << language_name << " word list: " << std::string(trimmed.data(), trimmed.size())); else - throw std::runtime_error("Duplicate prefix in " + language_name + " word list: " + trimmed); + throw std::runtime_error("Duplicate prefix in " + language_name + " word list: " + std::string(trimmed.data(), trimmed.size())); } trimmed_word_map[trimmed] = ii; } @@ -145,7 +150,7 @@ namespace Language * \brief Returns a pointer to the word map. * \return A pointer to the word map. */ - const std::unordered_map& get_word_map() const + const std::unordered_map& get_word_map() const { return word_map; } @@ -153,7 +158,7 @@ namespace Language * \brief Returns a pointer to the trimmed word map. * \return A pointer to the trimmed word map. */ - const std::unordered_map& get_trimmed_word_map() const + const std::unordered_map& get_trimmed_word_map() const { return trimmed_word_map; } diff --git a/src/simplewallet/simplewallet.cpp b/src/simplewallet/simplewallet.cpp index e07c7e49b..f87c2936d 100644 --- a/src/simplewallet/simplewallet.cpp +++ b/src/simplewallet/simplewallet.cpp @@ -152,12 +152,31 @@ namespace // // Note also that input for passwords is NOT translated, to remain compatible with any // passwords containing special characters that predate this switch to UTF-8 support. - static std::string cp850_to_utf8(const std::string &cp850_str) + template + static T cp850_to_utf8(const T &cp850_str) { boost::locale::generator gen; gen.locale_cache_enabled(true); std::locale loc = gen("en_US.CP850"); - return boost::locale::conv::to_utf(cp850_str, loc); + const boost::locale::conv::method_type how = boost::locale::conv::default_method; + T result; + const char *begin = cp850_str.data(); + const char *end = begin + cp850_str.size(); + result.reserve(end-begin); + typedef std::back_insert_iterator inserter_type; + inserter_type inserter(result); + boost::locale::utf::code_point c; + while(begin!=end) { + c=boost::locale::utf::utf_traits::template decode(begin,end); + if(c==boost::locale::utf::illegal || c==boost::locale::utf::incomplete) { + if(how==boost::locale::conv::stop) + throw boost::locale::conv::conversion_error(); + } + else { + boost::locale::utf::utf_traits::template encode(c,inserter); + } + } + return result; } #endif @@ -177,6 +196,28 @@ namespace return epee::string_tools::trim(buf); } + epee::wipeable_string input_secure_line(const std::string& prompt) + { +#ifdef HAVE_READLINE + rdln::suspend_readline pause_readline; +#endif + auto pwd_container = tools::password_container::prompt(false, prompt.c_str(), false); + if (!pwd_container) + { + MERROR("Failed to read secure line"); + return ""; + } + + epee::wipeable_string buf = pwd_container->password(); + +#ifdef WIN32 + buf = cp850_to_utf8(buf); +#endif + + buf.trim(); + return buf; + } + boost::optional password_prompter(const char *prompt, bool verify) { #ifdef HAVE_READLINE @@ -595,7 +636,7 @@ bool simple_wallet::spendkey(const std::vector &args/* = std::vecto bool simple_wallet::print_seed(bool encrypted) { bool success = false; - std::string seed; + epee::wipeable_string seed; bool ready, multisig; if (m_wallet->key_on_device()) @@ -2679,28 +2720,45 @@ bool simple_wallet::ask_wallet_create_if_needed() * \brief Prints the seed with a nice message * \param seed seed to print */ -void simple_wallet::print_seed(std::string seed) +void simple_wallet::print_seed(const epee::wipeable_string &seed) { success_msg_writer(true) << "\n" << tr("NOTE: the following 25 words can be used to recover access to your wallet. " "Write them down and store them somewhere safe and secure. Please do not store them in " "your email or on file storage services outside of your immediate control.\n"); - boost::replace_nth(seed, " ", 15, "\n"); - boost::replace_nth(seed, " ", 7, "\n"); // don't log - std::cout << seed << std::endl; + int space_index = 0; + size_t len = seed.size(); + for (const char *ptr = seed.data(); len--; ++ptr) + { + if (*ptr == ' ') + { + if (space_index == 15 || space_index == 7) + putchar('\n'); + else + putchar(*ptr); + ++space_index; + } + else + putchar(*ptr); + } + putchar('\n'); + fflush(stdout); } //---------------------------------------------------------------------------------------------------- -static bool might_be_partial_seed(std::string words) +static bool might_be_partial_seed(const epee::wipeable_string &words) { - std::vector seed; + std::vector seed; - boost::algorithm::trim(words); - boost::split(seed, words, boost::is_any_of(" "), boost::token_compress_on); + words.split(seed); return seed.size() < 24; } //---------------------------------------------------------------------------------------------------- bool simple_wallet::init(const boost::program_options::variables_map& vm) { + epee::misc_utils::auto_scope_leave_caller scope_exit_handler = epee::misc_utils::create_scope_leave_handler([&](){ + m_electrum_seed.wipe(); + }); + const bool testnet = tools::wallet2::has_testnet_option(vm); const bool stagenet = tools::wallet2::has_stagenet_option(vm); if (testnet && stagenet) @@ -2710,7 +2768,7 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm) } const network_type nettype = testnet ? TESTNET : stagenet ? STAGENET : MAINNET; - std::string multisig_keys; + epee::wipeable_string multisig_keys; if (!handle_command_line(vm)) return false; @@ -2752,8 +2810,8 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm) { if (m_restore_multisig_wallet) { - const char *prompt = "Specify multisig seed: "; - m_electrum_seed = input_line(prompt); + const char *prompt = "Specify multisig seed"; + m_electrum_seed = input_secure_line(prompt); if (std::cin.eof()) return false; if (m_electrum_seed.empty()) @@ -2767,8 +2825,8 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm) m_electrum_seed = ""; do { - const char *prompt = m_electrum_seed.empty() ? "Specify Electrum seed: " : "Electrum seed continued: "; - std::string electrum_seed = input_line(prompt); + const char *prompt = m_electrum_seed.empty() ? "Specify Electrum seed" : "Electrum seed continued"; + epee::wipeable_string electrum_seed = input_secure_line(prompt); if (std::cin.eof()) return false; if (electrum_seed.empty()) @@ -2776,18 +2834,21 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm) fail_msg_writer() << tr("specify a recovery parameter with the --electrum-seed=\"words list here\""); return false; } - m_electrum_seed += electrum_seed + " "; + m_electrum_seed += electrum_seed; + m_electrum_seed += ' '; } while (might_be_partial_seed(m_electrum_seed)); } } if (m_restore_multisig_wallet) { - if (!epee::string_tools::parse_hexstr_to_binbuff(m_electrum_seed, multisig_keys)) + const boost::optional parsed = m_electrum_seed.parse_hexstr(); + if (!parsed) { fail_msg_writer() << tr("Multisig seed failed verification"); return false; } + multisig_keys = *parsed; } else { @@ -2809,7 +2870,7 @@ bool simple_wallet::init(const boost::program_options::variables_map& vm) crypto::secret_key key; crypto::cn_slow_hash(seed_pass.data(), seed_pass.size(), (crypto::hash&)key); sc_reduce32((unsigned char*)key.data); - multisig_keys = m_wallet->decrypt(multisig_keys, key, true); + multisig_keys = m_wallet->decrypt(std::string(multisig_keys.data(), multisig_keys.size()), key, true); } else m_recovery_key = cryptonote::decrypt_key(m_recovery_key, seed_pass); @@ -3478,7 +3539,7 @@ boost::optional simple_wallet::new_wallet(const boost::pr } // convert rng value to electrum-style word list - std::string electrum_words; + epee::wipeable_string electrum_words; crypto::ElectrumWords::bytes_to_words(recovery_val, electrum_words, mnemonic_language); @@ -3586,7 +3647,7 @@ boost::optional simple_wallet::new_wallet(const boost::pr } //---------------------------------------------------------------------------------------------------- boost::optional simple_wallet::new_wallet(const boost::program_options::variables_map& vm, - const std::string &multisig_keys, const std::string &old_language) + const epee::wipeable_string &multisig_keys, const std::string &old_language) { auto rc = tools::wallet2::make_new(vm, password_prompter); m_wallet = std::move(rc.first); @@ -3697,7 +3758,7 @@ bool simple_wallet::open_wallet(const boost::program_options::variables_map& vm) m_wallet->rewrite(m_wallet_file, password); // Display the seed - std::string seed; + epee::wipeable_string seed; m_wallet->get_seed(seed); print_seed(seed); } diff --git a/src/simplewallet/simplewallet.h b/src/simplewallet/simplewallet.h index 472ad0c00..c8e17ea6e 100644 --- a/src/simplewallet/simplewallet.h +++ b/src/simplewallet/simplewallet.h @@ -44,6 +44,7 @@ #include "cryptonote_basic/cryptonote_basic_impl.h" #include "wallet/wallet2.h" #include "console_handler.h" +#include "wipeable_string.h" #include "common/i18n.h" #include "common/password.h" #include "crypto/crypto.h" // for definition of crypto::secret_key @@ -96,7 +97,7 @@ namespace cryptonote boost::optional new_wallet(const boost::program_options::variables_map& vm, const cryptonote::account_public_address& address, const boost::optional& spendkey, const crypto::secret_key& viewkey); boost::optional new_wallet(const boost::program_options::variables_map& vm, - const std::string &multisig_keys, const std::string &old_language); + const epee::wipeable_string &multisig_keys, const std::string &old_language); boost::optional new_wallet(const boost::program_options::variables_map& vm, const std::string& device_name); bool open_wallet(const boost::program_options::variables_map& vm); bool close_wallet(); @@ -238,7 +239,7 @@ namespace cryptonote * \brief Prints the seed with a nice message * \param seed seed to print */ - void print_seed(std::string seed); + void print_seed(const epee::wipeable_string &seed); /*! * \brief Gets the word seed language from the user. @@ -329,7 +330,7 @@ namespace cryptonote std::string m_import_path; std::string m_subaddress_lookahead; - std::string m_electrum_seed; // electrum-style seed parameter + epee::wipeable_string m_electrum_seed; // electrum-style seed parameter crypto::secret_key m_recovery_key; // recovery key (used as random for wallet gen) bool m_restore_deterministic_wallet; // recover flag diff --git a/src/wallet/api/wallet.cpp b/src/wallet/api/wallet.cpp index b48bf07e0..e75bee5c9 100644 --- a/src/wallet/api/wallet.cpp +++ b/src/wallet/api/wallet.cpp @@ -733,10 +733,10 @@ bool WalletImpl::close(bool store) std::string WalletImpl::seed() const { - std::string seed; + epee::wipeable_string seed; if (m_wallet) m_wallet->get_seed(seed); - return seed; + return std::string(seed.data(), seed.size()); // TODO } std::string WalletImpl::getSeedLanguage() const diff --git a/src/wallet/wallet2.cpp b/src/wallet/wallet2.cpp index 9deaad09b..e61e437c0 100644 --- a/src/wallet/wallet2.cpp +++ b/src/wallet/wallet2.cpp @@ -789,7 +789,7 @@ bool wallet2::is_deterministic() const return keys_deterministic; } //---------------------------------------------------------------------------------------------------- -bool wallet2::get_seed(std::string& electrum_words, const epee::wipeable_string &passphrase) const +bool wallet2::get_seed(epee::wipeable_string& electrum_words, const epee::wipeable_string &passphrase) const { bool keys_deterministic = is_deterministic(); if (!keys_deterministic) @@ -815,7 +815,7 @@ bool wallet2::get_seed(std::string& electrum_words, const epee::wipeable_string return true; } //---------------------------------------------------------------------------------------------------- -bool wallet2::get_multisig_seed(std::string& seed, const epee::wipeable_string &passphrase, bool raw) const +bool wallet2::get_multisig_seed(epee::wipeable_string& seed, const epee::wipeable_string &passphrase, bool raw) const { bool ready; uint32_t threshold, total; @@ -838,7 +838,7 @@ bool wallet2::get_multisig_seed(std::string& seed, const epee::wipeable_string & crypto::secret_key skey; crypto::public_key pkey; const account_keys &keys = get_account().get_keys(); - std::string data; + epee::wipeable_string data; data.append((const char*)&threshold, sizeof(uint32_t)); data.append((const char*)&total, sizeof(uint32_t)); skey = keys.m_spend_secret_key; @@ -864,7 +864,7 @@ bool wallet2::get_multisig_seed(std::string& seed, const epee::wipeable_string & if (raw) { - seed = epee::string_tools::buff_to_hex_nodelimer(data); + seed = epee::to_hex::wipeable_string({(const unsigned char*)data.data(), data.size()}); } else { @@ -3161,7 +3161,7 @@ bool wallet2::verify_password(const std::string& keys_file_name, const epee::wip * \param create_address_file Whether to create an address file */ void wallet2::generate(const std::string& wallet_, const epee::wipeable_string& password, - const std::string& multisig_data, bool create_address_file) + const epee::wipeable_string& multisig_data, bool create_address_file) { clear(); prepare_file_names(wallet_); @@ -10671,14 +10671,14 @@ size_t wallet2::import_multisig(std::vector blobs) return n_outputs; } //---------------------------------------------------------------------------------------------------- -std::string wallet2::encrypt(const std::string &plaintext, const crypto::secret_key &skey, bool authenticated) const +std::string wallet2::encrypt(const char *plaintext, size_t len, const crypto::secret_key &skey, bool authenticated) const { crypto::chacha_key key; crypto::generate_chacha_key(&skey, sizeof(skey), key, m_kdf_rounds); std::string ciphertext; crypto::chacha_iv iv = crypto::rand(); - ciphertext.resize(plaintext.size() + sizeof(iv) + (authenticated ? sizeof(crypto::signature) : 0)); - crypto::chacha20(plaintext.data(), plaintext.size(), key, iv, &ciphertext[sizeof(iv)]); + ciphertext.resize(len + sizeof(iv) + (authenticated ? sizeof(crypto::signature) : 0)); + crypto::chacha20(plaintext, len, key, iv, &ciphertext[sizeof(iv)]); memcpy(&ciphertext[0], &iv, sizeof(iv)); if (authenticated) { @@ -10692,12 +10692,28 @@ std::string wallet2::encrypt(const std::string &plaintext, const crypto::secret_ return ciphertext; } //---------------------------------------------------------------------------------------------------- +std::string wallet2::encrypt(const epee::span &plaintext, const crypto::secret_key &skey, bool authenticated) const +{ + return encrypt(plaintext.data(), plaintext.size(), skey, authenticated); +} +//---------------------------------------------------------------------------------------------------- +std::string wallet2::encrypt(const std::string &plaintext, const crypto::secret_key &skey, bool authenticated) const +{ + return encrypt(plaintext.data(), plaintext.size(), skey, authenticated); +} +//---------------------------------------------------------------------------------------------------- +std::string wallet2::encrypt(const epee::wipeable_string &plaintext, const crypto::secret_key &skey, bool authenticated) const +{ + return encrypt(plaintext.data(), plaintext.size(), skey, authenticated); +} +//---------------------------------------------------------------------------------------------------- std::string wallet2::encrypt_with_view_secret_key(const std::string &plaintext, bool authenticated) const { return encrypt(plaintext, get_account().get_keys().m_view_secret_key, authenticated); } //---------------------------------------------------------------------------------------------------- -std::string wallet2::decrypt(const std::string &ciphertext, const crypto::secret_key &skey, bool authenticated) const +template +T wallet2::decrypt(const std::string &ciphertext, const crypto::secret_key &skey, bool authenticated) const { const size_t prefix_size = sizeof(chacha_iv) + (authenticated ? sizeof(crypto::signature) : 0); THROW_WALLET_EXCEPTION_IF(ciphertext.size() < prefix_size, @@ -10706,8 +10722,6 @@ std::string wallet2::decrypt(const std::string &ciphertext, const crypto::secret crypto::chacha_key key; crypto::generate_chacha_key(&skey, sizeof(skey), key, m_kdf_rounds); const crypto::chacha_iv &iv = *(const crypto::chacha_iv*)&ciphertext[0]; - std::string plaintext; - plaintext.resize(ciphertext.size() - prefix_size); if (authenticated) { crypto::hash hash; @@ -10718,10 +10732,14 @@ std::string wallet2::decrypt(const std::string &ciphertext, const crypto::secret THROW_WALLET_EXCEPTION_IF(!crypto::check_signature(hash, pkey, signature), error::wallet_internal_error, "Failed to authenticate ciphertext"); } - crypto::chacha20(ciphertext.data() + sizeof(iv), ciphertext.size() - prefix_size, key, iv, &plaintext[0]); - return plaintext; + std::unique_ptr buffer{new char[ciphertext.size() - prefix_size]}; + auto wiper = epee::misc_utils::create_scope_leave_handler([&]() { memwipe(buffer.get(), ciphertext.size() - prefix_size); }); + crypto::chacha20(ciphertext.data() + sizeof(iv), ciphertext.size() - prefix_size, key, iv, buffer.get()); + return T(buffer.get(), ciphertext.size() - prefix_size); } //---------------------------------------------------------------------------------------------------- +template epee::wipeable_string wallet2::decrypt(const std::string &ciphertext, const crypto::secret_key &skey, bool authenticated) const; +//---------------------------------------------------------------------------------------------------- std::string wallet2::decrypt_with_view_secret_key(const std::string &ciphertext, bool authenticated) const { return decrypt(ciphertext, get_account().get_keys().m_view_secret_key, authenticated); diff --git a/src/wallet/wallet2.h b/src/wallet/wallet2.h index d04156461..86fee0da7 100644 --- a/src/wallet/wallet2.h +++ b/src/wallet/wallet2.h @@ -495,7 +495,7 @@ namespace tools * \param create_address_file Whether to create an address file */ void generate(const std::string& wallet_, const epee::wipeable_string& password, - const std::string& multisig_data, bool create_address_file = false); + const epee::wipeable_string& multisig_data, bool create_address_file = false); /*! * \brief Generates a wallet or restores one. @@ -637,7 +637,7 @@ namespace tools * \brief Checks if deterministic wallet */ bool is_deterministic() const; - bool get_seed(std::string& electrum_words, const epee::wipeable_string &passphrase = epee::wipeable_string()) const; + bool get_seed(epee::wipeable_string& electrum_words, const epee::wipeable_string &passphrase = epee::wipeable_string()) const; /*! * \brief Checks if light wallet. A light wallet sends view key to a server where the blockchain is scanned. @@ -691,7 +691,7 @@ namespace tools bool multisig(bool *ready = NULL, uint32_t *threshold = NULL, uint32_t *total = NULL) const; bool has_multisig_partial_key_images() const; bool has_unknown_key_images() const; - bool get_multisig_seed(std::string& seed, const epee::wipeable_string &passphrase = std::string(), bool raw = true) const; + bool get_multisig_seed(epee::wipeable_string& seed, const epee::wipeable_string &passphrase = std::string(), bool raw = true) const; bool key_on_device() const { return m_key_on_device; } // locked & unlocked balance of given or current subaddress account @@ -1054,9 +1054,12 @@ namespace tools void update_pool_state(bool refreshed = false); void remove_obsolete_pool_txs(const std::vector &tx_hashes); + std::string encrypt(const char *plaintext, size_t len, const crypto::secret_key &skey, bool authenticated = true) const; + std::string encrypt(const epee::span &span, const crypto::secret_key &skey, bool authenticated = true) const; std::string encrypt(const std::string &plaintext, const crypto::secret_key &skey, bool authenticated = true) const; + std::string encrypt(const epee::wipeable_string &plaintext, const crypto::secret_key &skey, bool authenticated = true) const; std::string encrypt_with_view_secret_key(const std::string &plaintext, bool authenticated = true) const; - std::string decrypt(const std::string &ciphertext, const crypto::secret_key &skey, bool authenticated = true) const; + template T decrypt(const std::string &ciphertext, const crypto::secret_key &skey, bool authenticated = true) const; std::string decrypt_with_view_secret_key(const std::string &ciphertext, bool authenticated = true) const; std::string make_uri(const std::string &address, const std::string &payment_id, uint64_t amount, const std::string &tx_description, const std::string &recipient_name, std::string &error) const; diff --git a/src/wallet/wallet_rpc_server.cpp b/src/wallet/wallet_rpc_server.cpp index 510cb3e58..136cd0e2d 100644 --- a/src/wallet/wallet_rpc_server.cpp +++ b/src/wallet/wallet_rpc_server.cpp @@ -1574,11 +1574,13 @@ namespace tools if (req.key_type.compare("mnemonic") == 0) { - if (!m_wallet->get_seed(res.key)) + epee::wipeable_string seed; + if (!m_wallet->get_seed(seed)) { er.message = "The wallet is non-deterministic. Cannot display seed."; return false; } + res.key = std::string(seed.data(), seed.size()); // send to the network, then wipe RAM :D } else if(req.key_type.compare("view_key") == 0) { diff --git a/tests/unit_tests/CMakeLists.txt b/tests/unit_tests/CMakeLists.txt index 3c7414640..f47192ea9 100644 --- a/tests/unit_tests/CMakeLists.txt +++ b/tests/unit_tests/CMakeLists.txt @@ -72,7 +72,8 @@ set(unit_tests_sources ringct.cpp output_selection.cpp vercmp.cpp - ringdb.cpp) + ringdb.cpp + wipeable_string.cpp) set(unit_tests_headers unit_tests_utils.h) diff --git a/tests/unit_tests/mnemonics.cpp b/tests/unit_tests/mnemonics.cpp index 8fa3192b9..0b74a6b94 100644 --- a/tests/unit_tests/mnemonics.cpp +++ b/tests/unit_tests/mnemonics.cpp @@ -27,6 +27,8 @@ // THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. #include "gtest/gtest.h" +#include "wipeable_string.h" +#include "mnemonics/language_base.h" #include "mnemonics/electrum-words.h" #include "crypto/crypto.h" #include @@ -74,14 +76,16 @@ namespace void test_language(const Language::Base &language) { const std::vector &word_list = language.get_word_list(); - std::string seed = "", return_seed = ""; + epee::wipeable_string w_seed = "", w_return_seed = ""; + std::string seed, return_seed; // Generate a random seed without checksum crypto::secret_key randkey; for (size_t ii = 0; ii < sizeof(randkey); ++ii) { randkey.data[ii] = rand(); } - crypto::ElectrumWords::bytes_to_words(randkey, seed, language.get_language_name()); + crypto::ElectrumWords::bytes_to_words(randkey, w_seed, language.get_language_name()); + seed = std::string(w_seed.data(), w_seed.size()); // remove the checksum word const char *space = strrchr(seed.c_str(), ' '); ASSERT_TRUE(space != NULL); @@ -103,7 +107,8 @@ namespace 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()); + crypto::ElectrumWords::bytes_to_words(key, w_return_seed, language.get_language_name()); + return_seed = std::string(w_return_seed.data(), w_return_seed.size()); ASSERT_EQ(true, res); std::cout << "Returned seed:\n"; std::cout << return_seed << std::endl; @@ -126,8 +131,9 @@ namespace 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()); + w_return_seed = ""; + crypto::ElectrumWords::bytes_to_words(key, w_return_seed, language.get_language_name()); + return_seed = std::string(w_return_seed.data(), w_return_seed.size()); ASSERT_EQ(true, res); std::cout << "Returned seed:\n"; std::cout << return_seed << std::endl; @@ -202,3 +208,17 @@ TEST(mnemonics, language_detection_with_bad_checksum) ASSERT_EQ(true, res); ASSERT_STREQ(language_name.c_str(), "Português"); } + +TEST(mnemonics, utf8prefix) +{ + ASSERT_TRUE(Language::utf8prefix(epee::wipeable_string("foo"), 0) == ""); + ASSERT_TRUE(Language::utf8prefix(epee::wipeable_string("foo"), 1) == "f"); + ASSERT_TRUE(Language::utf8prefix(epee::wipeable_string("foo"), 2) == "fo"); + ASSERT_TRUE(Language::utf8prefix(epee::wipeable_string("foo"), 3) == "foo"); + ASSERT_TRUE(Language::utf8prefix(epee::wipeable_string("foo"), 4) == "foo"); + ASSERT_TRUE(Language::utf8prefix(epee::wipeable_string("æon"), 0) == ""); + ASSERT_TRUE(Language::utf8prefix(epee::wipeable_string("æon"), 1) == "æ"); + ASSERT_TRUE(Language::utf8prefix(epee::wipeable_string("æon"), 2) == "æo"); + ASSERT_TRUE(Language::utf8prefix(epee::wipeable_string("æon"), 3) == "æon"); + ASSERT_TRUE(Language::utf8prefix(epee::wipeable_string("æon"), 4) == "æon"); +} diff --git a/tests/unit_tests/wipeable_string.cpp b/tests/unit_tests/wipeable_string.cpp new file mode 100644 index 000000000..5ea1c1729 --- /dev/null +++ b/tests/unit_tests/wipeable_string.cpp @@ -0,0 +1,204 @@ +// Copyright (c) 2018, 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 +#include +#include "gtest/gtest.h" + +#include "misc_log_ex.h" +#include "wipeable_string.h" + +TEST(wipeable_string, ctor) +{ + epee::wipeable_string s0; + ASSERT_EQ(s0.size(), 0); + + epee::wipeable_string s1(std::string("foo")); + ASSERT_EQ(s1.size(), 3); + ASSERT_TRUE(!memcmp(s1.data(), "foo", s1.size())); + + epee::wipeable_string s2(std::string("bar")); + ASSERT_EQ(s2.size(), 3); + ASSERT_TRUE(!memcmp(s2.data(), "bar", s2.size())); + + epee::wipeable_string s3(std::string("quux")); + ASSERT_EQ(s3.size(), 4); + ASSERT_TRUE(!memcmp(s3.data(), "quux", s3.size())); +} + +TEST(wipeable_string, wipe) +{ + epee::wipeable_string s0(std::string("foo")); + ASSERT_EQ(s0.size(), 3); + s0.wipe(); + ASSERT_EQ(s0.size(), 3); + ASSERT_TRUE(!memcmp(s0.data(), "\0\0\0", 3)); +} + +TEST(wipeable_string, clear) +{ + epee::wipeable_string s0(std::string("foo")); + ASSERT_EQ(s0.size(), 3); + s0.clear(); + ASSERT_EQ(s0.size(), 0); +} + +TEST(wipeable_string, push_back) +{ + epee::wipeable_string s0(std::string("fo")); + ASSERT_EQ(s0.size(), 2); + s0.push_back('o'); + ASSERT_EQ(s0.size(), 3); + ASSERT_TRUE(!memcmp(s0.data(), "foo", s0.size())); +} + +TEST(wipeable_string, append_char) +{ + epee::wipeable_string s0(std::string("fo")); + ASSERT_EQ(s0.size(), 2); + s0 += 'o'; + ASSERT_EQ(s0.size(), 3); + ASSERT_TRUE(!memcmp(s0.data(), "foo", s0.size())); +} + +TEST(wipeable_string, append_string) +{ + epee::wipeable_string s0(std::string("foo")); + ASSERT_EQ(s0.size(), 3); + s0 += "bar"; + ASSERT_EQ(s0.size(), 6); + ASSERT_TRUE(!memcmp(s0.data(), "foobar", s0.size())); +} + +TEST(wipeable_string, empty) +{ + epee::wipeable_string s0; + ASSERT_TRUE(s0.empty()); + s0.push_back(' '); + ASSERT_FALSE(s0.empty()); + ASSERT_EQ(s0.pop_back(), ' '); + ASSERT_TRUE(s0.empty()); +} + +TEST(wipeable_string, pop_back) +{ + epee::wipeable_string s = "test"; + ASSERT_EQ(s.size(), 4); + ASSERT_EQ(s.pop_back(), 't'); + ASSERT_EQ(s.size(), 3); + ASSERT_TRUE(!memcmp(s.data(), "tes", s.size())); +} + +TEST(wipeable_string, equal) +{ + epee::wipeable_string s0 = "foo"; + epee::wipeable_string s1 = "bar"; + epee::wipeable_string s0_2 = "foo"; + ASSERT_TRUE(s0 == s0); + ASSERT_TRUE(s0 == s0_2); + ASSERT_TRUE(s1 == s1); + ASSERT_FALSE(s1 == s0); + ASSERT_FALSE(s1 == s0_2); +} + +TEST(wipeable_string, not_equal) +{ + epee::wipeable_string s0 = "foo"; + epee::wipeable_string s1 = "bar"; + epee::wipeable_string s0_2 = "foo"; + ASSERT_FALSE(s0 != s0); + ASSERT_FALSE(s0 != s0_2); + ASSERT_FALSE(s1 != s1); + ASSERT_TRUE(s1 != s0); + ASSERT_TRUE(s1 != s0_2); +} + +static epee::wipeable_string trimmed(const char *s) +{ + epee::wipeable_string str(s); + str.trim(); + return str; +} + +TEST(wipeable_string, trim) +{ + ASSERT_TRUE(trimmed("") == ""); + ASSERT_TRUE(trimmed(" ") == ""); + ASSERT_TRUE(trimmed(" ") == ""); + ASSERT_TRUE(trimmed("a") == "a"); + ASSERT_TRUE(trimmed(" a") == "a"); + ASSERT_TRUE(trimmed(" a") == "a"); + ASSERT_TRUE(trimmed("a ") == "a"); + ASSERT_TRUE(trimmed("a ") == "a"); + ASSERT_TRUE(trimmed(" a ") == "a"); + ASSERT_TRUE(trimmed(" a ") == "a"); + ASSERT_TRUE(trimmed(" ab ") == "ab"); + ASSERT_TRUE(trimmed(" a b ") == "a b"); + ASSERT_TRUE(trimmed(" a b ") == "a b"); +} + +static bool check_split(const char *s, const std::vector &v) +{ + epee::wipeable_string str(s); + std::vector fields; + str.split(fields); + return v == fields; +} + +TEST(wipeable_string, split) +{ + ASSERT_TRUE(check_split("", {})); + ASSERT_TRUE(check_split("foo", {"foo"})); + ASSERT_TRUE(check_split(" foo ", {"foo"})); + ASSERT_TRUE(check_split("foo bar", {"foo", "bar"})); + ASSERT_TRUE(check_split("foo bar", {"foo", "bar"})); + ASSERT_TRUE(check_split("foo bar baz", {"foo", "bar", "baz"})); + ASSERT_TRUE(check_split(" foo bar baz ", {"foo", "bar", "baz"})); + ASSERT_TRUE(check_split(" foo bar baz", {"foo", "bar", "baz"})); + ASSERT_TRUE(check_split("foo bar baz ", {"foo", "bar", "baz"})); +} + +TEST(wipeable_string, parse_hexstr) +{ + boost::optional s; + + ASSERT_EQ(boost::none, epee::wipeable_string("x").parse_hexstr()); + ASSERT_EQ(boost::none, epee::wipeable_string("x0000000000000000").parse_hexstr()); + ASSERT_EQ(boost::none, epee::wipeable_string("0000000000000000x").parse_hexstr()); + ASSERT_EQ(boost::none, epee::wipeable_string("0").parse_hexstr()); + ASSERT_EQ(boost::none, epee::wipeable_string("000").parse_hexstr()); + + ASSERT_TRUE((s = epee::wipeable_string("").parse_hexstr())); + ASSERT_EQ(*s, ""); + ASSERT_TRUE((s = epee::wipeable_string("00").parse_hexstr())); + ASSERT_EQ(*s, epee::wipeable_string("", 1)); + ASSERT_TRUE((s = epee::wipeable_string("41").parse_hexstr())); + ASSERT_EQ(*s, epee::wipeable_string("A")); + ASSERT_TRUE((s = epee::wipeable_string("414243").parse_hexstr())); + ASSERT_EQ(*s, epee::wipeable_string("ABC")); +}