Add support for storing pending txes to file

This commit is contained in:
Lee Clagett
2025-06-23 18:33:14 -04:00
parent 3e453a2a02
commit b365726cbf
11 changed files with 591 additions and 253 deletions
+3
View File
@@ -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
)
+4
View File
@@ -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;
+2
View File
@@ -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.
+1
View File
@@ -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;
+127 -11
View File
@@ -28,6 +28,8 @@
#include "pending_transaction.h"
#include <filesystem>
#include <fstream>
#include <stdexcept>
#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<transaction>& dest)
{
if (!dest)
dest = std::make_shared<backend::transaction>();
read_bytes(src, *dest);
}
static void write_bytes(wire::writer& dest, const std::shared_ptr<transaction>& 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<std::shared_ptr<backend::transaction>> txes;
};
void read_bytes(wire::reader& src, std::shared_ptr<backend::transaction>& dest)
{
if (!dest)
dest = std::make_shared<backend::transaction>();
read_bytes(src, *dest);
}
void write_bytes(wire::writer& dest, const std::shared_ptr<backend::transaction> src)
{
if (!src)
WIRE_DLOG_THROW_(error::unexpected_nullptr);
write_bytes(dest, *src);
}
template<typename F, typename T>
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<cryptonote::transaction>&& source)
{
if (source)
@@ -48,31 +97,50 @@ namespace lwsf { namespace internal
return {};
}
}
pending_transaction::pending_transaction(std::shared_ptr<backend::wallet> wallet, std::string error, std::vector<std::shared_ptr<backend::transaction>> 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> pending_transaction::load_from_file(std::shared_ptr<backend::wallet> 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<pending_transaction>(wallet, "Invalid path/file for tx");
expect<epee::byte_slice> payload = epee::byte_slice{};
{
const boost::lock_guard<boost::mutex> lock{wallet->sync};
payload = decrypt(std::move(file), epee::as_byte_span(unwrap(unwrap(wallet->primary.view.sec))));
}
if (!payload)
return std::make_unique<pending_transaction>(wallet, payload.error().message());
txes_file dest{};
if (std::error_code error = wire::msgpack::from_bytes(std::move(*payload), dest))
return std::make_unique<pending_transaction>(wallet, error.message());
return std::make_unique<pending_transaction>(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<epee::byte_slice> 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<boost::mutex> 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<const char*>(bytes->data()), bytes->size());
return out.good();
}
std::uint64_t pending_transaction::amount() const
{
safe_uint64_t out{};
+7 -1
View File
@@ -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<backend::wallet> wallet, std::string error, std::vector<std::shared_ptr<backend::transaction>> local = {});
pending_transaction(std::shared_ptr<backend::wallet> wallet, std::string error, std::vector<std::shared_ptr<backend::transaction>> local = {});
static std::unique_ptr<pending_transaction> load_from_file(std::shared_ptr<backend::wallet> 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<std::string> signersKeys() const override final { return {}; }
};
WIRE_DECLARE_OBJECT(pending_transaction);
}} // lwsf // internal
+286
View File
@@ -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 <fstream>
#include <sodium/core.h>
#include <sodium/crypto_pwhash_argon2id.h>
#include <sodium/randombytes.h>
#include <sodium/crypto_aead_chacha20poly1305.h>
#include <stdexcept>
#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<std::uint8_t, 32> get_key(const epee::span<const std::uint8_t> password) const
{
if (sodium_init() < 0)
throw std::runtime_error{"Failed to initialize libsodium"};
std::array<std::uint8_t, key_size()> key{{}};
if (crypto_pwhash_argon2id(
key.data(), key.size(),
reinterpret_cast<const char*>(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<const std::uint8_t> 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<std::size_t>::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<const std::uint8_t> 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<std::size_t>::max() <= std::numeric_limits<unsigned long long>::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<typename F, typename T>
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<char*>(buffer.data()), buffer.size());
if (file.good())
return epee::byte_slice{std::move(buffer)};
return nullptr;
}
expect<epee::byte_slice> encrypt(const std::string_view file_magic, epee::byte_slice payload, const std::uint64_t iterations, const epee::span<const std::uint8_t> 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<epee::byte_slice> decrypt(epee::byte_slice encrypted, const epee::span<const std::uint8_t> 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
+49
View File
@@ -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 <cstdint>
#include <string>
#include <string_view>
#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<epee::byte_slice> encrypt(std::string_view file_magic, epee::byte_slice payload, std::uint64_t iterations, epee::span<const std::uint8_t> password);
//! Use `try_load` for to retrieve `contents`, then decrypt with `password`.
expect<epee::byte_slice> decrypt(epee::byte_slice contents, epee::span<const std::uint8_t> password);
}} // lwsf // internal
+90
View File
@@ -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 <cstdint>
#include <limits>
#include <stdexcept>
#include <type_traits>
namespace lwsf { namespace internal
{
template<typename T>
class safe_integer
{
static_assert(std::is_integral<T>());
T value_;
public:
constexpr safe_integer() noexcept
: value_(0)
{}
constexpr safe_integer(const T value) noexcept
: value_(value)
{}
template<typename U>
constexpr safe_integer(const U value) noexcept
: value_(value)
{
static_assert(std::is_same<U, void>(), "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<typename U>
constexpr operator U() const noexcept
{ static_assert(std::is_same<U, void>(), "invalid conversion"); };
safe_integer& add(safe_integer rhs)
{
if (std::numeric_limits<T>::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<T>::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<std::uint64_t>;
}} // lwsf // internal
+20 -239
View File
@@ -36,11 +36,8 @@
#include <boost/range/combine.hpp>
#include <exception>
#include <filesystem>
#include <fstream>
#include <sodium/core.h>
#include <sodium/crypto_pwhash_argon2id.h>
#include <sodium/randombytes.h>
#include <sodium/crypto_aead_chacha20poly1305.h>
#include <stdexcept>
#include <string_view>
#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<std::uint8_t, 32> get_key(const std::string& password) const
{
std::array<std::uint8_t, key_size()> 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<std::size_t>::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<std::size_t>::max() <= std::numeric_limits<unsigned long long>::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<typename F, typename T>
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<char*>(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<std::uint8_t>(password)));
}
std::vector<std::string> 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<std::uint8_t>(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<std::uint8_t>(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<std::vector<uint64_t>>{{*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;
+2 -2
View File
@@ -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;
/*!