Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
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