diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4a4afa2..f632ef3 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -39,6 +39,7 @@ set(lwsf_sources subaddress_minor.cpp transaction_history.cpp transaction_info.cpp + utils/encrypted_file.cpp wallet.cpp wallet_manager.cpp ) @@ -50,6 +51,8 @@ set(lwsf_headers rpc.h transaction_history.h transaction_info.h + utils/encrypted_file.h + utils/numeric.h wallet.h ) diff --git a/src/error.cpp b/src/error.cpp index 1133d4d..97d9cee 100644 --- a/src/error.cpp +++ b/src/error.cpp @@ -41,6 +41,8 @@ namespace lwsf return "Configuration issue"; case error::create: return "Account creation not possible"; + case error::decryption: + return "Unable to decrypt file (likely bad password)"; case error::import_fee: return "Import/restore from height needs payment, see address book"; case error::import_invalid: @@ -57,6 +59,8 @@ namespace lwsf return "lwsf (local) limits on subaddresses too small"; case error::unexpected_userinfo: return "Unexpected user+pass field in URL"; + case error::unexpected_nullptr: + return "Unexpected nullptr"; default: break; diff --git a/src/error.h b/src/error.h index c05db9e..7e513d4 100644 --- a/src/error.h +++ b/src/error.h @@ -37,6 +37,7 @@ namespace lwsf approval, //!< Account needs approval configuration, //!< Bad Configuration create, //!< Account creation not possible + decryption, //!< Failed to decrypt data import_fee, //!< `import_wallet_request` has fee import_invalid, //!< `import_wallet_request` has invalid fee import_pending, //!< `import_wallet_request` not fulfilled/approved @@ -46,6 +47,7 @@ namespace lwsf subaddr_disabled, //!< Server has subaddresses disabled subaddr_local, //!< Local limits on subaddresses too small unexpected_userinfo,//!< Unexpected user+pass provided + unexpected_nullptr, //!< Expected non-nullptr }; //! \return Error message string. diff --git a/src/lwsf_config.h b/src/lwsf_config.h index c5b83e2..abc2736 100644 --- a/src/lwsf_config.h +++ b/src/lwsf_config.h @@ -48,6 +48,7 @@ namespace lwsf { namespace config constexpr const lookahead default_minimal_lookahead{5, 15}; constexpr const std::string_view default_primary_name{"Primary account"}; constexpr const std::size_t initial_buffer_size = 1024 * 64; // 64 KiB + constexpr const std::size_t max_file_read_size = 50 * 1024 * 1024; // 50 MiB constexpr const std::size_t max_inputs_in_rpc = 512; constexpr const std::size_t max_outputs_in_construction = 16; constexpr const std::size_t max_ring_size_in_rpc = 128; diff --git a/src/pending_transaction.cpp b/src/pending_transaction.cpp index 39e0494..0edffb6 100644 --- a/src/pending_transaction.cpp +++ b/src/pending_transaction.cpp @@ -28,6 +28,8 @@ #include "pending_transaction.h" +#include +#include #include #include "backend.h" #include "byte_slice.h" // monero/contrib/epee/include @@ -35,12 +37,59 @@ #include "error.h" #include "numeric.h" #include "string_tools.h" // monero/contrib/epee/include - +#include "utils/encrypted_file.h" +#include "wire.h" +#include "wire/msgpack.h" +#include "wire/wrapper/trusted_array.h" namespace lwsf { namespace internal { + namespace backend + { + static void read_bytes(wire::reader& src, std::shared_ptr& dest) + { + if (!dest) + dest = std::make_shared(); + read_bytes(src, *dest); + } + + static void write_bytes(wire::writer& dest, const std::shared_ptr& src) + { + if (!src) + throw std::logic_error{"Unexpected nullptr in write_bytes"}; + write_bytes(dest, *src); + } + } + namespace { + constexpr std::string_view tx_file_magic{"lwsf-tx-1.0"}; + + struct txes_file + { + std::vector> txes; + }; + + void read_bytes(wire::reader& src, std::shared_ptr& dest) + { + if (!dest) + dest = std::make_shared(); + read_bytes(src, *dest); + } + void write_bytes(wire::writer& dest, const std::shared_ptr src) + { + if (!src) + WIRE_DLOG_THROW_(error::unexpected_nullptr); + write_bytes(dest, *src); + } + + template + void txes_map(F& format, T& self) + { + wire::object(format, wire::field("txes", wire::trusted_array(std::ref(self.txes)))); + } + WIRE_DEFINE_OBJECT(txes_file, txes_map); + cryptonote::transaction get_tx(expect&& source) { if (source) @@ -48,31 +97,50 @@ namespace lwsf { namespace internal return {}; } } + + pending_transaction::pending_transaction(std::shared_ptr wallet, std::string error, std::vector> local) : wallet_(std::move(wallet)), local_(std::move(local)), error_(std::move(error)) { if (!wallet_) throw std::invalid_argument{"lwsf::internal::pending_transaction cannot be given nullptr backend"}; - for (const auto& e : local) + for (const auto& e : local_) { if (!e) throw std::invalid_argument{"lwsf::internal::pending_transaction cannot be given nullptr backend::transaction"}; } } + std::unique_ptr pending_transaction::load_from_file(std::shared_ptr wallet, const std::string& filename) + { + if (!wallet) + throw std::invalid_argument{"lwsf::internal::pending_transaction::load_from_file cannot be given nullptr backend"}; + + epee::byte_slice file = try_load(filename, tx_file_magic); + if (file.empty()) + return std::make_unique(wallet, "Invalid path/file for tx"); + + expect payload = epee::byte_slice{}; + { + const boost::lock_guard lock{wallet->sync}; + payload = decrypt(std::move(file), epee::as_byte_span(unwrap(unwrap(wallet->primary.view.sec)))); + } + if (!payload) + return std::make_unique(wallet, payload.error().message()); + + txes_file dest{}; + if (std::error_code error = wire::msgpack::from_bytes(std::move(*payload), dest)) + return std::make_unique(wallet, error.message()); + return std::make_unique(wallet, "", std::move(dest.txes)); + } + pending_transaction::~pending_transaction() {} - int pending_transaction::status() const + bool pending_transaction::send() { - return error_.empty() ? Status_Ok : Status_Error; - } - - bool pending_transaction::commit(const std::string &filename, bool) - { - // saving to file not yet implemented - if (!error_.empty() || !filename.empty()) + if (!error_.empty()) return false; for (auto e : local_) @@ -88,10 +156,58 @@ namespace lwsf { namespace internal if (!entry->second) entry->second = std::move(e); } - return true; } + int pending_transaction::status() const + { + return error_.empty() ? Status_Ok : Status_Error; + } + + bool pending_transaction::commit(const std::string &filename, const bool overwrite) + { + assert(wallet_); // see constructor + if (!error_.empty()) + return false; + if (filename.empty()) + return send(); + + if (std::filesystem::exists(filename) && !overwrite) + { + error_ = "File already exists"; + return false; + } + + expect bytes = epee::byte_slice{}; + { + epee::byte_stream sink{}; + const txes_file source{local_}; + if (std::error_code error = wire::msgpack::to_bytes(sink, source)) + { + error_ = error.message(); + return false; + } + + const boost::lock_guard lock{wallet_->sync}; + bytes = encrypt( + tx_file_magic, + epee::byte_slice{std::move(sink)}, + 0 /* iterations */, + epee::as_byte_span(unwrap(unwrap(wallet_->primary.view.sec))) + ); + } + + if (!bytes) + { + error_ = bytes.error().message(); + return false; + } + + std::ofstream out{filename}; + out.write(reinterpret_cast(bytes->data()), bytes->size()); + return out.good(); + } + std::uint64_t pending_transaction::amount() const { safe_uint64_t out{}; diff --git a/src/pending_transaction.h b/src/pending_transaction.h index cb2ab21..0e0499c 100644 --- a/src/pending_transaction.h +++ b/src/pending_transaction.h @@ -37,6 +37,7 @@ #include "common/expect.h" // monero/src #include "cryptonote_basic/cryptonote_basic.h" // monero/src #include "wallet/api/wallet2_api.h" // moneor/src +#include "wire/fwd.h" namespace lwsf { namespace internal { @@ -53,7 +54,8 @@ namespace lwsf { namespace internal std::string error_; public: - pending_transaction(std::shared_ptr wallet, std::string error, std::vector> local = {}); + pending_transaction(std::shared_ptr wallet, std::string error, std::vector> local = {}); + static std::unique_ptr load_from_file(std::shared_ptr wallet, const std::string& filename); pending_transaction(pending_transaction&&) = delete; pending_transaction(const pending_transaction&) = delete; @@ -63,6 +65,8 @@ namespace lwsf { namespace internal pending_transaction& operator=(pending_transaction&&) = delete; pending_transaction& operator=(const pending_transaction&) = delete; + bool send(); + virtual int status() const override final; virtual std::string errorString() const override final { return error_; } virtual bool commit(const std::string &filename = "", bool overwrite = false) override final; @@ -98,4 +102,6 @@ namespace lwsf { namespace internal */ virtual std::vector signersKeys() const override final { return {}; } }; + + WIRE_DECLARE_OBJECT(pending_transaction); }} // lwsf // internal diff --git a/src/utils/encrypted_file.cpp b/src/utils/encrypted_file.cpp new file mode 100644 index 0000000..b85c551 --- /dev/null +++ b/src/utils/encrypted_file.cpp @@ -0,0 +1,286 @@ +// Copyright (c) 2025, 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 "encrypted_file.h" + +#include +#include +#include +#include +#include +#include + +#include "byte_stream.h" // monero/contrib/epee/include +#include "error.h" +#include "lwsf_config.h" +#include "wire.h" +#include "wire/msgpack.h" + +namespace lwsf { namespace internal +{ + namespace + { + struct encrypted_file + { + std::string cipher; //!< Name of cipher+authentication used + std::string pwhasher; //!< Name of pwhasher used + epee::byte_slice salt; //!< Pwhashing salt + epee::byte_slice nonce; //!< Encryption iv/nonce + epee::byte_slice epayload; //!< Encrypted contents + std::uint64_t iterations; //!< Pwhashing iterations + std::uint32_t memory; //!< Pwhashing memory + + encrypted_file() = delete; + + static epee::byte_slice get_random(const std::size_t length) + { + if (sodium_init() < 0) + throw std::runtime_error{"Failed to initialize libsodium"}; + + epee::byte_stream out; + out.put_n(0, length); + randombytes_buf(out.data(), length); + return epee::byte_slice{std::move(out)}; + } + + static constexpr const char* pwhasher_name() noexcept + { + return "argon2id"; + } + static constexpr int pwhash_algorithm() noexcept + { + return crypto_pwhash_argon2id_ALG_ARGON2ID13; + } + static constexpr std::size_t salt_size() noexcept + { + return crypto_pwhash_argon2id_SALTBYTES; + } + static constexpr std::uint64_t ops_min() noexcept { return 5; } + static constexpr std::size_t memory_limit() noexcept { return 7 * 1024 * 1024; } + static epee::byte_slice get_salt() { return get_random(salt_size()); } + + static constexpr const char* cipher_name() noexcept + { + return "chacha20-poly1305_ietf"; + } + static constexpr unsigned long long max_size() noexcept + { + return crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX; + } + static constexpr std::size_t key_size() noexcept + { + return crypto_aead_chacha20poly1305_ietf_KEYBYTES; + } + static constexpr std::size_t tag_size() noexcept + { + return crypto_aead_chacha20poly1305_ietf_ABYTES; + } + static constexpr std::size_t nonce_size() noexcept + { + return crypto_aead_chacha20poly1305_ietf_NPUBBYTES; + } + static epee::byte_slice get_nonce() { return get_random(nonce_size()); } + + std::array get_key(const epee::span password) const + { + if (sodium_init() < 0) + throw std::runtime_error{"Failed to initialize libsodium"}; + + std::array key{{}}; + if (crypto_pwhash_argon2id( + key.data(), key.size(), + reinterpret_cast(password.data()), password.size(), + salt.data(), + iterations, memory, pwhash_algorithm()) != 0) + { + throw std::runtime_error{std::string{pwhasher_name()} + " failed"}; + } + return key; + } + + static encrypted_file make(const epee::byte_slice& upayload, const std::uint64_t iterations, const epee::span password) + { + if (sodium_init() < 0) + throw std::runtime_error{"Failed to initialize libsodium"}; + + if (max_size() < upayload.size()) + throw std::runtime_error{std::string{"Exceeded max size for "} + cipher_name()}; + if (std::numeric_limits::max() - upayload.size() < tag_size()) + throw std::runtime_error{"Exceeded max size_t after authentication tag"}; + + encrypted_file out{ + cipher_name(), + pwhasher_name(), + get_salt(), + get_nonce(), + nullptr, + std::max(iterations, ops_min()), + memory_limit() + }; + + const auto key = out.get_key(password); + + epee::byte_stream buffer; + buffer.put_n(0, upayload.size() + tag_size()); + + unsigned long long out_bytes = buffer.size(); + if (crypto_aead_chacha20poly1305_ietf_encrypt( + buffer.data(), std::addressof(out_bytes), + upayload.data(), upayload.size(), + nullptr, 0, + nullptr, + out.nonce.data(), key.data()) != 0) + { + // this should basically never happen, so throw + throw std::runtime_error{std::string{cipher_name()} + " encryption failed"}; + } + + out.epayload = epee::byte_slice{std::move(buffer)}.take_slice(out_bytes); + return out; + } + + //! \return Unencrypted payload, or `nullptr`. + epee::byte_slice get_payload(const epee::span password) const + { + if (nonce.size() != nonce_size()) + return nullptr; + if (salt.size() != salt_size()) + return nullptr; + if (cipher != cipher_name()) + return nullptr; + if (pwhasher != pwhasher_name()) + return nullptr; + if (max_size() < epayload.size()) + return nullptr; + + if (sodium_init() < 0) + throw std::runtime_error{"Failed to initialize libsodium"}; + + const auto key = get_key(password); + static_assert(key_size() == key.size()); + + epee::byte_stream buffer; + buffer.put_n(0, epayload.size()); + + static_assert( + std::numeric_limits::max() <= std::numeric_limits::max() + ); + + unsigned long long out_bytes = buffer.size(); + if (crypto_aead_chacha20poly1305_ietf_decrypt( + buffer.data(), std::addressof(out_bytes), + nullptr, + epayload.data(), epayload.size(), + nullptr, 0, + nonce.data(), key.data()) != 0) + { + return nullptr; + } + + return epee::byte_slice{std::move(buffer)}.take_slice(out_bytes); + } + }; + + template + void map_encrypted_file(F& format, T& self) + { + wire::object(format, + WIRE_FIELD(cipher), + WIRE_FIELD(pwhasher), + WIRE_FIELD(salt), + WIRE_FIELD(nonce), + WIRE_FIELD(epayload), + WIRE_FIELD(iterations), + WIRE_FIELD(memory) + ); + } + + WIRE_DEFINE_OBJECT(encrypted_file, map_encrypted_file); + } + + epee::byte_slice try_load(const std::string& filename, const std::string_view file_magic) + { + std::ifstream file{filename, std::ios::binary}; + if (!file.is_open()) + return nullptr; + + { + std::string magic; + magic.resize(file_magic.size()); + file.read(magic.data(), magic.size()); + if (!file.good() || magic != file_magic) + return nullptr; + } + + file.seekg(0, std::ios::end); + const auto size = file.tellg(); + if (size < 0 || std::size_t(size) < file_magic.size()) + return nullptr; + + if (config::max_file_read_size < std::size_t(size)) + return nullptr; + + epee::byte_stream buffer; + buffer.put_n(0, std::size_t(size) - file_magic.size()); + + file.seekg(file_magic.size()); + file.read(reinterpret_cast(buffer.data()), buffer.size()); + + if (file.good()) + return epee::byte_slice{std::move(buffer)}; + return nullptr; + } + + expect encrypt(const std::string_view file_magic, epee::byte_slice payload, const std::uint64_t iterations, const epee::span password) + { + const auto contents = encrypted_file::make(payload, iterations, password); + const std::size_t payload_size = payload.size(); + payload = nullptr; // free up some memory that is no longer needed + + epee::byte_stream buffer; + buffer.reserve(payload_size + file_magic.size() + 2048); + buffer.write(file_magic.data(), file_magic.size()); + + if (std::error_code error = wire::msgpack::to_bytes(buffer, contents)) + return error; + return epee::byte_slice{std::move(buffer)}; + } + + expect decrypt(epee::byte_slice encrypted, const epee::span password) + { + encrypted_file contents{}; + if (std::error_code error = wire::msgpack::from_bytes(std::move(encrypted), contents)) + return error; + auto payload = contents.get_payload(epee::to_span(password)); + if (payload.empty()) + return {error::decryption}; + return payload; + } + +}} // lwsf // internal + diff --git a/src/utils/encrypted_file.h b/src/utils/encrypted_file.h new file mode 100644 index 0000000..bf91786 --- /dev/null +++ b/src/utils/encrypted_file.h @@ -0,0 +1,49 @@ +// Copyright (c) 2025, 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 + +#include +#include +#include + +#include "byte_slice.h" // monero/contrib/epee/include +#include "common/expect.h" // monero/src +#include "span.h" // monero/contrib/epee/include + +namespace lwsf { namespace internal +{ + //! \return `filename` contents with `file_magic` verified and stripped off. + epee::byte_slice try_load(const std::string& filename, std::string_view file_magic); + + //! `file_magic` is pre-pended to file bytes for identification. + expect encrypt(std::string_view file_magic, epee::byte_slice payload, std::uint64_t iterations, epee::span password); + + //! Use `try_load` for to retrieve `contents`, then decrypt with `password`. + expect decrypt(epee::byte_slice contents, epee::span password); +}} // lwsf // internal diff --git a/src/utils/numeric.h b/src/utils/numeric.h new file mode 100644 index 0000000..1b5e0ec --- /dev/null +++ b/src/utils/numeric.h @@ -0,0 +1,90 @@ +// Copyright (c) 2025, 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 + +#include +#include +#include +#include + +namespace lwsf { namespace internal +{ + template + class safe_integer + { + static_assert(std::is_integral()); + T value_; + public: + constexpr safe_integer() noexcept + : value_(0) + {} + + constexpr safe_integer(const T value) noexcept + : value_(value) + {} + + template + constexpr safe_integer(const U value) noexcept + : value_(value) + { + static_assert(std::is_same(), "invalid conversion"); + } + + constexpr safe_integer(const safe_integer&) noexcept = default; + constexpr safe_integer& operator=(const safe_integer&) noexcept = default; + + constexpr operator T() const noexcept { return value_; } + + template + constexpr operator U() const noexcept + { static_assert(std::is_same(), "invalid conversion"); }; + + safe_integer& add(safe_integer rhs) + { + if (std::numeric_limits::max() - value_ < rhs.value_) + throw std::overflow_error{"safe_integer overflow in addition"}; + value_ += rhs.value_; + return *this; + } + + safe_integer& subtract(safe_integer rhs) + { + if (value_ - std::numeric_limits::min() < rhs.value_) + throw std::underflow_error{"safe_integer underflow in subtraction"}; + value_ -= rhs.value_; + return *this; + } + + safe_integer& operator+=(safe_integer rhs) { return add(rhs); } + safe_integer& operator-=(safe_integer rhs) { return subtract(rhs); } + }; + + using safe_uint64_t = safe_integer; +}} // lwsf // internal + diff --git a/src/wallet.cpp b/src/wallet.cpp index eb10b6f..21be7be 100644 --- a/src/wallet.cpp +++ b/src/wallet.cpp @@ -36,11 +36,8 @@ #include #include #include -#include #include -#include #include -#include #include #include #include "address_book.h" @@ -61,6 +58,7 @@ #include "subaddress_account.h" #include "subaddress_minor.h" #include "transaction_history.h" +#include "utils/encrypted_file.h" #include "wire.h" #include "wire/msgpack.h" @@ -77,7 +75,7 @@ namespace lwsf { namespace internal namespace { //! The stored wallet file always has this at beginning - static constexpr std::string_view file_magic{"lwsf-wallet-1.0"}; + static constexpr std::string_view wallet_file_magic{"lwsf-wallet-1.0"}; struct null_connector { @@ -180,209 +178,6 @@ namespace lwsf { namespace internal return calculate_fee_from_weight(base_fee, estimate_tx_weight(n_inputs, n_outputs, mixin, extra_size), fee_mask); } - struct encrypted_file - { - std::string cipher; //!< Name of cipher+authentication used - std::string pwhasher; //!< Name of pwhasher used - epee::byte_slice salt; //!< Pwhashing salt - epee::byte_slice nonce; //!< Encryption iv/nonce - epee::byte_slice epayload; //!< Encrypted contents - std::uint64_t iterations; //!< Pwhashing iterations - std::uint32_t memory; //!< Pwhashing memory - - encrypted_file() = delete; - - //! \pre `sodium_init()` has been called - static epee::byte_slice get_random(const std::size_t length) - { - epee::byte_stream out; - out.put_n(0, length); - randombytes_buf(out.data(), length); - return epee::byte_slice{std::move(out)}; - } - - static constexpr const char* pwhasher_name() noexcept - { - return "argon2id"; - } - static constexpr int pwhash_algorithm() noexcept - { - return crypto_pwhash_argon2id_ALG_ARGON2ID13; - } - static constexpr std::size_t salt_size() noexcept - { - return crypto_pwhash_argon2id_SALTBYTES; - } - static constexpr std::uint64_t ops_min() noexcept { return 5; } - static constexpr std::size_t memory_limit() noexcept { return 7 * 1024 * 1024; } - static epee::byte_slice get_salt() { return get_random(salt_size()); } - - static constexpr const char* cipher_name() noexcept - { - return "chacha20-poly1305_ietf"; - } - static constexpr unsigned long long max_size() noexcept - { - return crypto_aead_chacha20poly1305_ietf_MESSAGEBYTES_MAX; - } - static constexpr std::size_t key_size() noexcept - { - return crypto_aead_chacha20poly1305_ietf_KEYBYTES; - } - static constexpr std::size_t tag_size() noexcept - { - return crypto_aead_chacha20poly1305_ietf_ABYTES; - } - static constexpr std::size_t nonce_size() noexcept - { - return crypto_aead_chacha20poly1305_ietf_NPUBBYTES; - } - static epee::byte_slice get_nonce() { return get_random(nonce_size()); } - - //! \pre `sodium_init()` has been called - std::array get_key(const std::string& password) const - { - std::array key{{}}; - if (crypto_pwhash_argon2id( - key.data(), key.size(), - password.data(), password.size(), - salt.data(), - iterations, memory, pwhash_algorithm()) != 0) - { - throw std::runtime_error{std::string{pwhasher_name()} + " failed"}; - } - return key; - } - - static encrypted_file - make(const epee::byte_slice& upayload, const std::uint64_t iterations, const std::string& password) - { - if (sodium_init() < 0) - throw std::runtime_error{"Failed to initialize libsodium"}; - - if (max_size() < upayload.size()) - throw std::runtime_error{std::string{"Exceeded max size for "} + cipher_name()}; - if (std::numeric_limits::max() - upayload.size() < tag_size()) - throw std::runtime_error{"Exceeded max size_t after authentication tag"}; - - encrypted_file out{ - cipher_name(), - pwhasher_name(), - get_salt(), - get_nonce(), - nullptr, - std::max(iterations, ops_min()), - memory_limit() - }; - - const auto key = out.get_key(password); - - epee::byte_stream buffer; - buffer.put_n(0, upayload.size() + tag_size()); - - unsigned long long out_bytes = buffer.size(); - if (crypto_aead_chacha20poly1305_ietf_encrypt( - buffer.data(), std::addressof(out_bytes), - upayload.data(), upayload.size(), - nullptr, 0, - nullptr, - out.nonce.data(), key.data()) != 0) - { - throw std::runtime_error{std::string{cipher_name()} + " encryption failed"}; - } - - out.epayload = epee::byte_slice{std::move(buffer)}.take_slice(out_bytes); - return out; - } - - //! \return Unencrypted payload, or `nullptr`. - epee::byte_slice get_payload(const std::string& password) const - { - if (nonce.size() != nonce_size()) - return nullptr; - if (salt.size() != salt_size()) - return nullptr; - if (cipher != cipher_name()) - return nullptr; - if (pwhasher != pwhasher_name()) - return nullptr; - if (max_size() < epayload.size()) - return nullptr; - - if (sodium_init() < 0) - throw std::runtime_error{"Failed to initialize libsodium"}; - - const auto key = get_key(password); - static_assert(key_size() == key.size()); - - epee::byte_stream buffer; - buffer.put_n(0, epayload.size()); - - static_assert( - std::numeric_limits::max() <= std::numeric_limits::max() - ); - - unsigned long long out_bytes = buffer.size(); - if (crypto_aead_chacha20poly1305_ietf_decrypt( - buffer.data(), std::addressof(out_bytes), - nullptr, - epayload.data(), epayload.size(), - nullptr, 0, - nonce.data(), key.data()) != 0) - { - throw std::runtime_error{std::string{cipher_name()} + " decryption failed"}; - } - - return epee::byte_slice{std::move(buffer)}.take_slice(out_bytes); - } - }; - - template - void map_encrypted_file(F& format, T& self) - { - wire::object(format, - WIRE_FIELD(cipher), - WIRE_FIELD(pwhasher), - WIRE_FIELD(salt), - WIRE_FIELD(nonce), - WIRE_FIELD(epayload), - WIRE_FIELD(iterations), - WIRE_FIELD(memory) - ); - } - - WIRE_DEFINE_OBJECT(encrypted_file, map_encrypted_file); - - epee::byte_slice try_load(const std::string& filename) - { - std::ifstream file{filename, std::ios::binary}; - if (!file.is_open()) - return nullptr; - - { - std::string magic; - magic.resize(file_magic.size()); - file.read(magic.data(), magic.size()); - if (!file.good() || magic != file_magic) - return nullptr; - } - - file.seekg(0, std::ios::end); - const auto size = file.tellg(); - if (size < 0 || std::size_t(size) < file_magic.size()) - return nullptr; - - epee::byte_stream buffer; - buffer.put_n(0, std::size_t(size) - file_magic.size()); - - file.seekg(file_magic.size()); - file.read(reinterpret_cast(buffer.data()), buffer.size()); - - if (file.good()) - return epee::byte_slice{std::move(buffer)}; - return nullptr; - } - #if defined (__unix__) || (defined (__APPLE__) && defined (__MACH__)) bool atomic_file_write(const std::filesystem::path& filename, const std::filesystem::path& directory, epee::byte_slice contents) noexcept { @@ -571,7 +366,7 @@ namespace lwsf { namespace internal bool wallet::is_wallet_file(const std::string& path) { - return !try_load(path).empty(); + return !try_load(path, wallet_file_magic).empty(); } bool wallet::verify_password(std::string path, const std::string& password) @@ -581,14 +376,10 @@ namespace lwsf { namespace internal if (keys_suffix.size() <= path.size() && std::string_view{path.data() + path.size() - keys_suffix.size()} == keys_suffix) path.erase(path.size() - keys_suffix.size()); - epee::byte_slice file = try_load(path); + epee::byte_slice file = try_load(path, wallet_file_magic); if (file.empty()) return false; - - encrypted_file contents{}; - if (wire::msgpack::from_bytes(std::move(file), contents)) - return false; - return !contents.get_payload(password).empty(); + return bool(decrypt(try_load(path, wallet_file_magic), epee::strspan(password))); } std::vector wallet::find(const std::string& path) @@ -604,7 +395,7 @@ namespace lwsf { namespace internal if (std::filesystem::is_regular_file(itr->status())) { std::string filename = itr->path().filename().string(); - if (!try_load(filename).empty()) + if (!try_load(filename, wallet_file_magic).empty()) out.push_back(std::move(filename)); } } @@ -667,19 +458,14 @@ namespace lwsf { namespace internal { try { - epee::byte_slice file = try_load(filename_ + ".new"); + epee::byte_slice file = try_load(filename_ + ".new", wallet_file_magic); if (file.empty()) - file = try_load(filename_); + file = try_load(filename_, wallet_file_magic); if (file.empty()) throw std::runtime_error{"Unable to open wallet file " + filename_}; - encrypted_file contents{}; - if (std::error_code error = wire::msgpack::from_bytes(std::move(file), contents)) - throw std::system_error{error}; - - epee::byte_slice payload = contents.get_payload(password_); - if (std::error_code error = data_->from_bytes(std::move(payload))) + if (std::error_code error = data_->from_bytes(decrypt(std::move(file), epee::strspan(password_)).value())) throw std::system_error{error}; // lock not needed; data_ was created and unique to us @@ -714,9 +500,6 @@ namespace lwsf { namespace internal thread_state_(state::stop), mandatory_refresh_(false) { - if (sodium_init() < 0) - throw std::runtime_error{"Failed to initialize libsodium"}; - crypto::secret_key recovery; std::string language; if (!crypto::ElectrumWords::words_to_bytes(mnemonic, recovery, language)) @@ -926,18 +709,6 @@ namespace lwsf { namespace internal try { - epee::byte_slice payload = data_->to_bytes().value(); - const auto contents = encrypted_file::make(payload, iterations_, password_); - const auto payload_size = payload.size(); - payload = nullptr; // free up some memory that is no longer needed - - epee::byte_stream buffer; - buffer.reserve(payload_size + 2048); - buffer.write(file_magic.data(), file_magic.size()); - - if (std::error_code error = wire::msgpack::to_bytes(buffer, contents)) - throw std::system_error{error}; - const std::filesystem::path file = real_path; const std::filesystem::path new_file = real_path + ".new"; const std::filesystem::path directory = @@ -947,7 +718,9 @@ namespace lwsf { namespace internal std::filesystem::rename(new_file, file); // blocks until file and directory contents are synced - if (!atomic_file_write(new_file, directory, epee::byte_slice{std::move(buffer)})) + epee::byte_slice blob = + encrypt(wallet_file_magic, data_->to_bytes().value(), iterations_, epee::strspan(password_)).value(); + if (!atomic_file_write(new_file, directory, std::move(blob))) throw std::runtime_error{"Failed to write file " + real_path}; std::filesystem::rename(new_file, file); @@ -1958,6 +1731,14 @@ namespace lwsf { namespace internal return createTransactionMultDest({dst_addr}, payment_id, amount ? std::optional>{{*amount}} : std::nullopt, mixin_count, priority, subaddr_account, subaddr_indices); } + bool wallet::submitTransaction(const std::string &fileName) + { + const auto tx = pending_transaction::load_from_file(data_, fileName); + if (!tx) + throw std::runtime_error{"Expected non-null from load_from_file"}; + return tx->status() == Monero::PendingTransaction::Status_Ok && tx->send(); + } + void wallet::disposeTransaction(Monero::PendingTransaction * t) { delete t; diff --git a/src/wallet.h b/src/wallet.h index 39f552a..efbb04f 100644 --- a/src/wallet.h +++ b/src/wallet.h @@ -449,7 +449,7 @@ namespace internal * @param signData encrypted unsigned transaction. Obtained with PendingTransaction::multisigSignData * @return PendingTransaction */ - virtual Monero::PendingTransaction* restoreMultisigTransaction(const std::string& signData) override { /* TODO */ return nullptr; } + virtual Monero::PendingTransaction* restoreMultisigTransaction(const std::string& signData) override { /* TODO */ return nullptr; } /*! * \brief createTransactionMultDest creates transaction with multiple destinations. if dst_addr is an integrated address, payment_id is ignored @@ -508,7 +508,7 @@ namespace internal * \brief submitTransaction - submits transaction in signed tx file * \return - true on success */ - virtual bool submitTransaction(const std::string &fileName) override { /* TODO */ return false; } + virtual bool submitTransaction(const std::string &fileName) override; /*!