Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/crypto/chacha.h
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@

#if defined(__cplusplus)
#include <memory.h>
#include <vector>
namespace crypto
{
// all C function share the same space of names regardless of 'namespace', but here we put it into crypto just for convenience -- sowle
Expand Down Expand Up @@ -117,7 +118,6 @@ namespace crypto
chacha20(&pod_object, sizeof pod_object, key, iv, &pod_object);
}


//
// legacy functions
//
Expand Down
99 changes: 99 additions & 0 deletions src/crypto/wallet_kdf.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
// Copyright (c) 2014-2026 Zano Project
// Distributed under the MIT/X11 software license, see the accompanying
// file COPYING or http://www.opensource.org/licenses/mit-license.php.

#pragma once

#include <stdint.h>
#include <stddef.h>
#include <memory.h>
#include <string>
#include <vector>

namespace crypto
{
extern "C"
{
#include "keccak.h"
}

// memory-hard password stretching for wallet file encryption
//
// construction is the ROMix algorithm from scrypt (C. Percival, 2009, "Stronger Key Derivation via Sequential Memory-Hard Functions"),
// adapted to use Keccak-256 instead of scrypt's PBKDF2/Salsa20 core so we don't pull in any new crypto primitive
inline void derive_key_romix_keccak(const char (&hdss)[32],
const void* password_data, size_t password_data_size,
const void* salt_data, size_t salt_data_size,
uint8_t N_log2, uint8_t phase2_log2_reduction,
uint8_t (&out_stretched)[32])
{
if (N_log2 < 10)
N_log2 = 10; // 32kb ram min
if (N_log2 > 30)
N_log2 = 30; // 32gb ram max
const uint64_t N = (uint64_t)1 << N_log2;
const uint64_t mask = N - 1; // N is power of 2 -> "x & mask" == "x % N"

if (phase2_log2_reduction >= N_log2)
phase2_log2_reduction = (uint8_t)(N_log2 - 1);
const uint64_t phase2_iters = N >> phase2_log2_reduction;

// phase 1 - sequential fill of V (scrypt-style SMix step 1)
// V[0] = keccak(hdss || salt || password)
// V[i] = keccak(V[i-1] || i_le64) for i in 1..N
// serial dependency chain prevents shortcut computation of later blocks
std::vector<uint8_t> V(N * 32);
{
std::string seed; // [hdss 32B][salt 16B][password ?B]
seed.reserve(32 + salt_data_size + password_data_size);
seed.append(hdss, 32);
if (salt_data_size)
seed.append(reinterpret_cast<const char*>(salt_data), salt_data_size);
if (password_data_size)
seed.append(reinterpret_cast<const char*>(password_data), password_data_size);
keccak(reinterpret_cast<const uint8_t*>(seed.data()), (int)seed.size(), V.data(), 32);
if (!seed.empty())
memset(const_cast<char*>(seed.data()), 0, seed.size());
}

uint8_t block_in[40]; // 32 (prev block) + 8 (i_le64) = 40
for (uint64_t i = 1; i < N; ++i)
{
memcpy(block_in, V.data() + (i - 1) * 32, 32);
for (int b = 0; b < 8; ++b)
block_in[32 + b] = (uint8_t)(i >> (8 * b));
keccak(block_in, 40, V.data() + i * 32, 32);
}

// phase 2 - data-dependent random walk (scrypt-style SMix step 2)
// X = V[N-1]
// for j in 0..phase2_iters:
// idx = first 8 bytes of X, mod N
// X = keccak((X XOR V[idx]) || j_le64)
// read index depends on the current X, so V cannot be streamed -
// the attacker must keep V resident or pay the TMTO penalty
uint8_t X[32];
memcpy(X, V.data() + (N - 1) * 32, 32);
uint8_t mix[40];
for (uint64_t j = 0; j < phase2_iters; ++j)
{
uint64_t idx_raw = 0;
for (int b = 0; b < 8; ++b)
idx_raw |= (uint64_t)X[b] << (8 * b);
const uint64_t idx = idx_raw & mask;
const uint8_t* Vi = V.data() + idx * 32;
for (int k = 0; k < 32; ++k)
mix[k] = X[k] ^ Vi[k];
for (int b = 0; b < 8; ++b)
mix[32 + b] = (uint8_t)(j >> (8 * b));
keccak(mix, 40, X, 32);
}

memcpy(out_stretched, X, 32);

// wipe buffs
memset(V.data(), 0, V.size());
memset(X, 0, sizeof(X));
memset(mix, 0, sizeof(mix));
}
}
1 change: 1 addition & 0 deletions src/currency_core/crypto_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -56,3 +56,4 @@ static_assert(crypto::bpp_crypto_trait_ZC_out::c_bpp_values_max == CURRENCY_TX_M

#define CRYPTO_HDS_CHACHA_WALLET_HEADER "ZANO_HDS_CHACHA_WALLET_HEADER__"
#define CRYPTO_HDS_CHACHA_WALLET_BODY "ZANO_HDS_CHACHA_WALLET_BODY____"
#define CRYPTO_HDS_WALLET_KDF_ROMIX "ZANO_HDS_WALLET_KDF_ROMIX______"
7 changes: 7 additions & 0 deletions src/currency_core/currency_config.h
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,13 @@
#define WALLET_FILE_BINARY_HEADER_VERSION_INITAL 1000
#define WALLET_FILE_BINARY_HEADER_VERSION_2 1001
#define WALLET_FILE_BINARY_HEADER_VERSION_3 1002
#define WALLET_FILE_BINARY_HEADER_VERSION_4 1003

#define WALLET_KDF_ALGO_NONE 0
#define WALLET_KDF_ALGO_ROMIX_KECCAK 1
#define WALLET_KDF_ROMIX_N_LOG2 20 //phase 1: the buffer size is V = 2^(N_log2) * 32 bytes, where N_log2 = 20 -> 1 million blocks * 32 bytes = 32 MiB
#define WALLET_KDF_ROMIX_PHASE2_LOG2_REDUCTION 3 //phase 2: iteration reduction: 0 = full N phase 2 iterations, 1 = N/2 iterations 2 = N/4, 3 = N/8, 4 = N/16 ... (still 32 MiB at N_log2=20)
#define WALLET_KDF_SALT_SIZE 16

#define WALLET_FILE_MAX_KEYS_SIZE 10000 //
#define WALLET_BRAIN_DATE_OFFSET 1543622400
Expand Down
76 changes: 64 additions & 12 deletions src/wallet/wallet2.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ using namespace epee;

#include "common/boost_serialization_helper.h"
#include "crypto/crypto.h"
#include "crypto/wallet_kdf.h"
#include "serialization/binary_utils.h"
#include "currency_core/bc_payments_id_service.h"
#include "version.h"
Expand Down Expand Up @@ -3013,7 +3014,25 @@ bool wallet2::reset_all()
return true;
}
//----------------------------------------------------------------------------------------------------
bool wallet2::store_keys(std::string& buff, const std::string& password, wallet2::keys_file_data& keys_file_data, bool store_as_watch_only /* = false */)
namespace
{
std::string derive_wallet_password_stretched(const std::string& password, const tools::wallet2::keys_file_data& kf_data)
{
if (kf_data.kdf_algo == WALLET_KDF_ALGO_NONE)
return password;
CHECK_AND_ASSERT_THROW_MES(kf_data.kdf_algo == WALLET_KDF_ALGO_ROMIX_KECCAK, "unsupported wallet KDF algo: " << (int)kf_data.kdf_algo);
CHECK_AND_ASSERT_THROW_MES(kf_data.kdf_salt.size() == WALLET_KDF_SALT_SIZE, "unexpected wallet KDF salt size: " << kf_data.kdf_salt.size());

uint8_t stretched[32];
crypto::derive_key_romix_keccak(CRYPTO_HDS_WALLET_KDF_ROMIX, password.data(), password.size(),
kf_data.kdf_salt.data(), kf_data.kdf_salt.size(), kf_data.kdf_N_log2, kf_data.kdf_phase2_log2_reduction, stretched);
std::string result(reinterpret_cast<const char*>(stretched), sizeof(stretched));
memset(stretched, 0, sizeof(stretched));
return result;
}
}
//----------------------------------------------------------------------------------------------------
bool wallet2::store_keys(std::string& buff, const std::string& password, wallet2::keys_file_data& keys_file_data, bool store_as_watch_only /* = false */, std::string* out_body_password /* = nullptr */)
{
currency::account_base acc = m_account;
if (store_as_watch_only)
Expand All @@ -3023,18 +3042,28 @@ bool wallet2::store_keys(std::string& buff, const std::string& password, wallet2
bool r = epee::serialization::store_t_to_binary(acc, account_data);
WLT_CHECK_AND_ASSERT_MES(r, false, "failed to serialize wallet keys");


keys_file_data.kdf_algo = WALLET_KDF_ALGO_ROMIX_KECCAK;
keys_file_data.kdf_N_log2 = WALLET_KDF_ROMIX_N_LOG2;
keys_file_data.kdf_phase2_log2_reduction = WALLET_KDF_ROMIX_PHASE2_LOG2_REDUCTION;
keys_file_data.kdf_salt.resize(WALLET_KDF_SALT_SIZE);
crypto::generate_random_bytes(keys_file_data.kdf_salt.size(), keys_file_data.kdf_salt.data());

// memory-hard stretch
const std::string stretched = derive_wallet_password_stretched(password, keys_file_data);

crypto::chacha_key key;
crypto::chacha_generate_key_and_iv(CRYPTO_HDS_CHACHA_WALLET_HEADER, password.data(), password.size(), 0, key);
crypto::chacha_generate_key_and_iv(CRYPTO_HDS_CHACHA_WALLET_HEADER, stretched.data(), stretched.size(), 0, key);
std::string cipher;
cipher.resize(account_data.size());
keys_file_data.iv = crypto::rand<crypto::chacha_iv>();
keys_file_data.iv = crypto::rand<crypto::chacha_iv>();
crypto::chacha20(account_data.data(), account_data.size(), key, keys_file_data.iv, &cipher[0]);
keys_file_data.account_data = cipher;

r = ::serialization::dump_binary(keys_file_data, buff);

if (out_body_password)
*out_body_password = stretched;

return true;
}
//----------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -3134,7 +3163,7 @@ bool wallet2::prepare_file_names(const std::wstring& file_path)
return true;
}
//----------------------------------------------------------------------------------------------------
void wallet2::load_keys(const std::string& buff, const std::string& password, uint64_t file_signature, keys_file_data& kf_data)
void wallet2::load_keys(const std::string& buff, const std::string& password, uint64_t file_signature, keys_file_data& kf_data, std::string* out_body_password)
{
bool r = false;
if (file_signature == WALLET_FILE_SIGNATURE_OLD)
Expand All @@ -3149,22 +3178,32 @@ void wallet2::load_keys(const std::string& buff, const std::string& password, ui
}
THROW_IF_TRUE_WALLET_EX(!r, error::wallet_internal_error, "internal error: failed to deserialize");
std::string account_data;

std::string body_password = password;

if (kf_data.version <= 1)
{
crypto::chacha_key key;
crypto::generate_chacha_key_legacy(password, key);
account_data.resize(kf_data.account_data.size());
crypto::chacha8(kf_data.account_data.data(), kf_data.account_data.size(), key, kf_data.iv, &account_data[0]);
}
else
else if (kf_data.version == 2)
{
//version 2
//legacy version 2
crypto::chacha_key key;
crypto::chacha_generate_key_and_iv(CRYPTO_HDS_CHACHA_WALLET_HEADER, password.data(), password.size(), 0, key);
account_data.resize(kf_data.account_data.size());
crypto::chacha20(kf_data.account_data.data(), kf_data.account_data.size(), key, kf_data.iv, &account_data[0]);
}
else
{
// version 3: ROMix-like Keccak password stretching drives the existing chacha KDF
body_password = derive_wallet_password_stretched(password, kf_data);
crypto::chacha_key key;
crypto::chacha_generate_key_and_iv(CRYPTO_HDS_CHACHA_WALLET_HEADER, body_password.data(), body_password.size(), 0, key);
account_data.resize(kf_data.account_data.size());
crypto::chacha20(kf_data.account_data.data(), kf_data.account_data.size(), key, kf_data.iv, &account_data[0]);
}

const currency::account_keys& keys = m_account.get_keys();
r = epee::serialization::load_t_from_binary(m_account, account_data);
Expand All @@ -3178,6 +3217,8 @@ void wallet2::load_keys(const std::string& buff, const std::string& password, ui
WLT_LOG_L0("Wrong password for wallet " << string_encoding::convert_to_ansii(m_wallet_file));
tools::error::throw_wallet_ex<error::invalid_password>(std::string(__FILE__ ":" STRINGIZE(__LINE__)));
}
if (out_body_password)
*out_body_password = body_password;
init_log_prefix();
}
//----------------------------------------------------------------------------------------------------
Expand Down Expand Up @@ -3300,7 +3341,8 @@ void wallet2::load(const std::wstring& wallet_, const std::string& password, boo
keys_buff.resize(wbh.m_cb_keys);
data_file.read((char*)keys_buff.data(), wbh.m_cb_keys);
wallet2::keys_file_data kf_data = AUTO_VAL_INIT(kf_data);
load_keys(keys_buff, password, wbh.m_signature, kf_data);
std::string body_password;
load_keys(keys_buff, password, wbh.m_signature, kf_data, &body_password);

bool need_to_resync = false;
if (wbh.m_ver == WALLET_FILE_BINARY_HEADER_VERSION_INITAL)
Expand All @@ -3327,6 +3369,15 @@ void wallet2::load(const std::wstring& wallet_, const std::string& password, boo
need_to_resync = !tools::portable_unserialize_obj_from_stream(*this, in);
WLT_LOG_L1("Detected format: WALLET_FILE_BINARY_HEADER_VERSION_3 (need_to_resync=" << need_to_resync << ")");
}
else if (wbh.m_ver == WALLET_FILE_BINARY_HEADER_VERSION_4)
{
tools::encrypt_chacha20_in_filter decrypt_filter(body_password, kf_data.iv, CRYPTO_HDS_CHACHA_WALLET_BODY);
boost::iostreams::filtering_istream in;
in.push(decrypt_filter);
in.push(data_file);
need_to_resync = !tools::portable_unserialize_obj_from_stream(*this, in);
WLT_LOG_L1("Detected format: WALLET_FILE_BINARY_HEADER_VERSION_4 (need_to_resync=" << need_to_resync << ")");
}
else
{
WLT_LOG_L0("Unknown wallet body version(" << wbh.m_ver << "), resync initiated.");
Expand Down Expand Up @@ -3379,16 +3430,17 @@ void wallet2::store(const std::wstring& path_to_save, const std::string& passwor

//prepare data
std::string keys_buff;
std::string body_password;
wallet2::keys_file_data keys_file_data = AUTO_VAL_INIT(keys_file_data);
bool r = store_keys(keys_buff, password, keys_file_data, m_watch_only);
bool r = store_keys(keys_buff, password, keys_file_data, m_watch_only, &body_password);
WLT_THROW_IF_FALSE_WALLET_CMN_ERR_EX(r, "failed to store_keys for wallet " << ascii_path_to_save);

//store data
wallet_file_binary_header wbh = AUTO_VAL_INIT(wbh);
wbh.m_signature = WALLET_FILE_SIGNATURE_V2;
wbh.m_cb_keys = keys_buff.size();
//@#@ change it to proper
wbh.m_ver = WALLET_FILE_BINARY_HEADER_VERSION_3;
wbh.m_ver = WALLET_FILE_BINARY_HEADER_VERSION_4;
std::string header_buff((const char*)&wbh, sizeof(wbh));

uint64_t ts = m_core_runtime_config.get_core_time();
Expand All @@ -3404,7 +3456,7 @@ void wallet2::store(const std::wstring& path_to_save, const std::string& passwor

WLT_LOG_L0("Storing to temporary file " << tmp_file_path.string() << " ...");
//creating encryption stream
tools::encrypt_chacha20_out_filter decrypt_filter(m_password, keys_file_data.iv, CRYPTO_HDS_CHACHA_WALLET_BODY);
tools::encrypt_chacha20_out_filter decrypt_filter(body_password, keys_file_data.iv, CRYPTO_HDS_CHACHA_WALLET_BODY);
boost::iostreams::filtering_ostream out;
out.push(decrypt_filter);
out.push(data_file);
Expand Down
18 changes: 15 additions & 3 deletions src/wallet/wallet2.h
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,11 @@ namespace tools
crypto::chacha_iv iv;
std::string account_data;

uint8_t kdf_algo{0};
uint8_t kdf_N_log2{0};
std::string kdf_salt;
uint8_t kdf_phase2_log2_reduction{0};

static keys_file_data from_old(const keys_file_data_old& v)
{
keys_file_data result = AUTO_VAL_INIT(result);
Expand All @@ -301,11 +306,18 @@ namespace tools
return result;
}

DEFINE_SERIALIZATION_VERSION(2)
DEFINE_SERIALIZATION_VERSION(4)
BEGIN_SERIALIZE_OBJECT()
VERSION_ENTRY(version)
FIELD(iv)
FIELD(account_data)
if (version >= 3)
{
FIELD(kdf_algo)
FIELD(kdf_N_log2)
FIELD(kdf_salt)
FIELD(kdf_phase2_log2_reduction)
}
END_SERIALIZE()
};

Expand Down Expand Up @@ -383,7 +395,7 @@ namespace tools
void store(const std::wstring& path);
void store(const std::wstring& path, const std::string& password);
void store_watch_only(const std::wstring& path, const std::string& password) const;
bool store_keys(std::string& buff, const std::string& password, wallet2::keys_file_data& keys_file_data, bool store_as_watch_only = false);
bool store_keys(std::string& buff, const std::string& password, wallet2::keys_file_data& keys_file_data, bool store_as_watch_only = false, std::string* out_body_password = nullptr);
std::wstring get_wallet_path()const { return m_wallet_file; }
std::string get_wallet_password()const { return m_password; }
currency::account_base& get_account() { return m_account; }
Expand Down Expand Up @@ -809,7 +821,7 @@ namespace tools
// ------------------------------------------------------------------------------------
void add_transfers_to_expiration_list(const std::vector<uint64_t>& selected_transfers, const std::vector<payment_details_subtransfer>& received, uint64_t expiration, const crypto::hash& related_tx_id);
void remove_transfer_from_expiration_list(uint64_t transfer_index);
void load_keys(const std::string& keys_file_name, const std::string& password, uint64_t file_signature, keys_file_data& kf_data);
void load_keys(const std::string& keys_file_name, const std::string& password, uint64_t file_signature, keys_file_data& kf_data, std::string* out_body_password = nullptr);
void process_ado_in_new_transaction(const currency::asset_descriptor_operation& ado, process_transaction_context& ptc);
void process_new_transaction(const currency::transaction& tx, uint64_t height, const currency::block& b, const std::vector<uint64_t>* pglobal_indexes);
void fetch_tx_global_indixes(const currency::transaction& tx, std::vector<uint64_t>& goutputs_indexes);
Expand Down
Loading