3 -> 5 reserved bits

removed network type
pull/1/head
tevador 4 years ago
parent 41546fa019
commit 7609d2ee96

@ -10,7 +10,7 @@ make
## Features ## Features
* embedded wallet birthday to optimize restoring from the seed (only blocks after the wallet birthday have to be scanned for transactions) * embedded wallet birthday to optimize restoring from the seed (only blocks after the wallet birthday have to be scanned for transactions)
* embedded network type (mainnet/stagenet/testnet) to prevent accidental misuse of the seed on a different network * 5 bits reserved for future updates
* advanced checksum based on Reed-Solomon linear code, which allows certain types of errors to be detected without false positives and provides limited error correction capability * advanced checksum based on Reed-Solomon linear code, which allows certain types of errors to be detected without false positives and provides limited error correction capability
* built-in way to make seeds incompatible between different coins, e.g. a seed for Aeon cannot be accidentally used to restore a Monero wallet * built-in way to make seeds incompatible between different coins, e.g. a seed for Aeon cannot be accidentally used to restore a Monero wallet
@ -19,15 +19,14 @@ make
### Create a new seed ### Create a new seed
``` ```
> ./monero-seed --create [--coin <monero|aeon>] [--net <MAIN|STAGE|TEST>] [--date <yyyy-MM-dd>] > ./monero-seed --create [--date <yyyy-MM-dd>] [--coin <monero|aeon>]
``` ```
Example: Example:
``` ```
> ./monero-seed --create --coin monero --net MAIN --date 2100/03/14 > ./monero-seed --create --date 2100/03/14 --coin monero
Mnemonic phrase: test park taste security oxygen decorate essence ridge ship fish vehicle dream fluid pattern Mnemonic phrase: test park taste security oxygen decorate essence ridge ship fish vehicle dream fluid pattern
- coin: monero - coin: monero
- network: MAIN
- private key: 7b816d8134e29393b0333eed4b6ed6edf97c156ad139055a706a6fb9599dcf8c - private key: 7b816d8134e29393b0333eed4b6ed6edf97c156ad139055a706a6fb9599dcf8c
- created on or after: 02/Mar/2100 - created on or after: 02/Mar/2100
``` ```
@ -42,7 +41,6 @@ Example:
``` ```
> ./monero-seed --restore "test park taste security oxygen decorate essence ridge ship fish vehicle dream fluid pattern" --coin monero > ./monero-seed --restore "test park taste security oxygen decorate essence ridge ship fish vehicle dream fluid pattern" --coin monero
- coin: monero - coin: monero
- network: MAIN
- private key: 7b816d8134e29393b0333eed4b6ed6edf97c156ad139055a706a6fb9599dcf8c - private key: 7b816d8134e29393b0333eed4b6ed6edf97c156ad139055a706a6fb9599dcf8c
- created on or after: 02/Mar/2100 - created on or after: 02/Mar/2100
``` ```
@ -60,7 +58,6 @@ This can be tested by replacing a word with `xxxx`:
> ./monero-seed --restore "test park xxxx security oxygen decorate essence ridge ship fish vehicle dream fluid pattern" --coin monero > ./monero-seed --restore "test park xxxx security oxygen decorate essence ridge ship fish vehicle dream fluid pattern" --coin monero
Warning: corrected erasure: xxxx -> taste Warning: corrected erasure: xxxx -> taste
- coin: monero - coin: monero
- network: MAIN
- private key: 7b816d8134e29393b0333eed4b6ed6edf97c156ad139055a706a6fb9599dcf8c - private key: 7b816d8134e29393b0333eed4b6ed6edf97c156ad139055a706a6fb9599dcf8c
- created on or after: 02/Mar/2100 - created on or after: 02/Mar/2100
``` ```
@ -69,8 +66,7 @@ Warning: corrected erasure: xxxx -> taste
The mnemonic phrase contains 154 bits of data, which are used as follows: The mnemonic phrase contains 154 bits of data, which are used as follows:
* 2 bits for the network type * 5 bits reserved for future use
* 3 bits reserved for future use
* 10 bits for approximate wallet birthday * 10 bits for approximate wallet birthday
* 128 bits for the private key seed * 128 bits for the private key seed
* 11 bits for checksum * 11 bits for checksum
@ -80,36 +76,28 @@ The mnemonic phrase contains 154 bits of data, which are used as follows:
The mnemonic phrase uses the BIP-39 wordlist, which has 2048 words, allowing 11 bits to be stored in each word. It has some additional useful properties, The mnemonic phrase uses the BIP-39 wordlist, which has 2048 words, allowing 11 bits to be stored in each word. It has some additional useful properties,
for example each word can be uniquly identified by its first 4 characters. The wordlist is available for 9 languages (this repository only uses the English list). for example each word can be uniquly identified by its first 4 characters. The wordlist is available for 9 languages (this repository only uses the English list).
### Network type
The network type is stored in 2 bits as follows:
* `00` = mainnet
* `01` = stagenet
* `10` = testnet
* `11` = invalid
The "Iinvalid" value can be used to support future extensions of the mnemonic seed to more than 14 words. Setting these two bits to `11` will prevent the first 14-words of a longer seed from being a valid 14-word seed (the checksum alone cannot prevent this).
### Reserved bits ### Reserved bits
There are 3 reserved bits for future use. Since there is no dedicated "version" field, the current implementation requires all reserved bits to be set to `0` for backwards compatibility. There are 5 reserved bits for future use. Possible use cases for the reserved bits include:
Possible use cases for the reserved bits include:
* a flag to differentiate between normal and "short" address format (with view key equal to the spend key) * a flag to differentiate between normal and "short" address format (with view key equal to the spend key)
* different KDF algorithms for generating the private key * different KDF algorithms for generating the private key
* seed encrypted with a passphrase * seed encrypted with a passphrase
Backwards compatibility is achieved under these two conditions:
1. Reserved (unused) bits are required to be 0. The software should return an error otherwise.
2. When defining a new feature bit, 0 should be the previous behavior.
### Wallet birthday ### Wallet birthday
The mnemonic phrase doesn't store block height but the approximate date when the wallet was created. This allows the seed to be generated offline without access to the blockchain. Wallet software can easily convert a date to the corresponding block height when restoring a seed. The mnemonic phrase stores the approximate date when the wallet was created. This allows the seed to be generated offline without access to the blockchain. Wallet software can easily convert a date to the corresponding block height when restoring a seed.
The wallet creation date has a resolution of 2629746 seconds (1/12 of the average Gregorian year). All dates between June 2020 and September 2105 can be represented. The wallet birthday has a resolution of 2629746 seconds (1/12 of the average Gregorian year). All dates between June 2020 and September 2105 can be represented.
### Private key seed ### Private key seed
The private key is derived from the 128-bit seed using PBKDF2-HMAC-SHA256 with 4096 iterations.The wallet birthday and network type are used as a salt. 128-bit seed provides the same level of security as the elliptic curve used by Monero. The private key is derived from the 128-bit seed using PBKDF2-HMAC-SHA256 with 4096 iterations.The wallet birthday and the 5 reserved/feature bits are used as a salt. 128-bit seed provides the same level of security as the elliptic curve used by Monero.
Future extensions may define other KDFs. Future extensions may define other KDFs.

@ -59,7 +59,6 @@ void print_seed(const monero_seed& seed, const char* coin, bool phrase) {
std::cout << "Mnemonic phrase: " << seed << std::endl; std::cout << "Mnemonic phrase: " << seed << std::endl;
} }
std::cout << "- coin: " << coin << std::endl; std::cout << "- coin: " << coin << std::endl;
std::cout << "- network: " << seed.net_name() << std::endl;
std::cout << "- private key: " << seed.key() << std::endl; std::cout << "- private key: " << seed.key() << std::endl;
auto created_on = seed.date(); auto created_on = seed.date();
std::tm tm = *std::localtime(&created_on); std::tm tm = *std::localtime(&created_on);
@ -69,12 +68,10 @@ void print_seed(const monero_seed& seed, const char* coin, bool phrase) {
int main(int argc, const char* argv[]) { int main(int argc, const char* argv[]) {
bool create; bool create;
const char* create_date; const char* create_date;
const char* create_net;
const char* coin; const char* coin;
const char* restore; const char* restore;
read_option("--create", argc, argv, create); read_option("--create", argc, argv, create);
read_string_option("--date", argc, argv, &create_date); read_string_option("--date", argc, argv, &create_date);
read_string_option("--net", argc, argv, &create_net, "MAIN");
read_string_option("--coin", argc, argv, &coin, "monero"); read_string_option("--coin", argc, argv, &coin, "monero");
read_string_option("--restore", argc, argv, &restore); read_string_option("--restore", argc, argv, &restore);
@ -87,7 +84,7 @@ int main(int argc, const char* argv[]) {
else { else {
time = std::time(nullptr); time = std::time(nullptr);
} }
monero_seed seed(time, coin, create_net); monero_seed seed(time, coin);
print_seed(seed, coin, true); print_seed(seed, coin, true);
} }
else if (restore != nullptr) { else if (restore != nullptr) {
@ -97,7 +94,7 @@ int main(int argc, const char* argv[]) {
else { else {
std::cout << "Monero 14-word mnemonic seed proof of concept" << std::endl; std::cout << "Monero 14-word mnemonic seed proof of concept" << std::endl;
std::cout << "Usage: " << std::endl; std::cout << "Usage: " << std::endl;
std::cout << argv[0] << " --create [--coin <monero|aeon>] [--net <MAIN|STAGE|TEST>] [--date <yyyy-MM-dd>]" << std::endl; std::cout << argv[0] << " --create [--date <yyyy-MM-dd>] [--coin <monero|aeon>]" << std::endl;
std::cout << argv[0] << " --restore \"<14-word seed>\" [--coin <monero|aeon>]" << std::endl; std::cout << argv[0] << " --restore \"<14-word seed>\" [--coin <monero|aeon>]" << std::endl;
} }
} }

@ -47,9 +47,7 @@ constexpr std::time_t time_step = 2629746; //30.436875 days = 1/12 of the Gregor
constexpr unsigned date_bits = 10; constexpr unsigned date_bits = 10;
constexpr unsigned date_mask = (1u << date_bits) - 1; constexpr unsigned date_mask = (1u << date_bits) - 1;
constexpr unsigned net_bits = 2; constexpr unsigned reserved_bits = 5;
constexpr unsigned net_mask = (1u << net_bits) - 1;
constexpr unsigned reserved_bits = 3;
constexpr unsigned reserved_mask = (1u << reserved_bits) - 1; constexpr unsigned reserved_mask = (1u << reserved_bits) - 1;
constexpr unsigned check_digits = 1; constexpr unsigned check_digits = 1;
constexpr unsigned checksum_size = gf_elem::size() * check_digits; constexpr unsigned checksum_size = gf_elem::size() * check_digits;
@ -64,16 +62,11 @@ static const std::string COIN_AEON = "aeon";
constexpr gf_elem monero_flag = gf_elem(0x539); constexpr gf_elem monero_flag = gf_elem(0x539);
constexpr gf_elem aeon_flag = gf_elem(0x201); constexpr gf_elem aeon_flag = gf_elem(0x201);
constexpr int flag_word = 1;
static const char* net_types[] = {
"MAIN", "STAGE", "TEST", nullptr
};
static const char* KDF_PBKDF2 = "PBKDF2-HMAC-SHA256/4096"; static const char* KDF_PBKDF2 = "PBKDF2-HMAC-SHA256/4096";
static_assert(total_bits static_assert(total_bits
== net_bits + reserved_bits + date_bits + checksum_size + == reserved_bits + date_bits + checksum_size +
sizeof(monero_seed::secret_seed) * CHAR_BIT, sizeof(monero_seed::secret_seed) * CHAR_BIT,
"Invalid mnemonic seed size"); "Invalid mnemonic seed size");
@ -119,32 +112,21 @@ static gf_elem get_coin_flag(const std::string& coin) {
static const reed_solomon_code rs(check_digits); static const reed_solomon_code rs(check_digits);
monero_seed::monero_seed(std::time_t date_created, const std::string& coin, const std::string& net) { monero_seed::monero_seed(std::time_t date_created, const std::string& coin) {
if (date_created < epoch) { if (date_created < epoch) {
THROW_EXCEPTION("date_created must not be before 1st June 2020"); THROW_EXCEPTION("date_created must not be before 1st June 2020");
} }
unsigned quantized_date = ((date_created - epoch) / time_step) & date_mask; unsigned quantized_date = ((date_created - epoch) / time_step) & date_mask;
date_ = epoch + quantized_date * time_step; date_ = epoch + quantized_date * time_step;
gf_elem coin_flag = get_coin_flag(coin); gf_elem coin_flag = get_coin_flag(coin);
net_name_ = nullptr;
for (int i = 0; i < net_mask; ++i) {
if (net_types[i] == net) {
net_type_ = i;
net_name_ = net_types[i];
}
}
if (net_name_ == nullptr) {
THROW_EXCEPTION("invalid network type");
}
reserved_ = 0; reserved_ = 0;
secure_random::gen_bytes(seed_.data(), seed_.size()); secure_random::gen_bytes(seed_.data(), seed_.size());
uint8_t salt[25] = "Monero 14-word seed"; uint8_t salt[25] = "Monero 14-word seed";
salt[20] = net_type_; salt[20] = reserved_;
store32(salt + 21, quantized_date); store32(salt + 21, quantized_date);
//argon2id_hash_raw(argon_tcost, argon_mcost, 1, seed_.data(), seed_.size(), salt, sizeof(salt), key_.data(), key_.size()); //argon2id_hash_raw(argon_tcost, argon_mcost, 1, seed_.data(), seed_.size(), salt, sizeof(salt), key_.data(), key_.size());
pbkdf2_hmac_sha256(seed_.data(), seed_.size(), salt, sizeof(salt), pbkdf2_iterations, key_.data(), key_.size()); pbkdf2_hmac_sha256(seed_.data(), seed_.size(), salt, sizeof(salt), pbkdf2_iterations, key_.data(), key_.size());
unsigned rem_bits = gf_elem::size(); unsigned rem_bits = gf_elem::size();
write_data(message_, rem_bits, net_type_, net_bits);
write_data(message_, rem_bits, reserved_, reserved_bits); write_data(message_, rem_bits, reserved_, reserved_bits);
write_data(message_, rem_bits, quantized_date, date_bits); write_data(message_, rem_bits, quantized_date, date_bits);
for (auto byte : seed_) { for (auto byte : seed_) {
@ -152,7 +134,7 @@ monero_seed::monero_seed(std::time_t date_created, const std::string& coin, cons
} }
assert(rem_bits == 0); assert(rem_bits == 0);
rs.encode(message_); rs.encode(message_);
message_[flag_word] -= coin_flag; message_[check_digits] -= coin_flag;
} }
monero_seed::monero_seed(const std::string& phrase, const std::string& coin) { monero_seed::monero_seed(const std::string& phrase, const std::string& coin) {
@ -191,17 +173,17 @@ monero_seed::monero_seed(const std::string& phrase, const std::string& coin) {
if (error >= 0) { if (error >= 0) {
for (unsigned i = 0; i < gf_2048::elements(); ++i) { for (unsigned i = 0; i < gf_2048::elements(); ++i) {
message_[error] = i; message_[error] = i;
message_[flag_word] += coin_flag; message_[check_digits] += coin_flag;
if (rs.check(message_)) { if (rs.check(message_)) {
correction_ = wordlist::english.get_word(i); correction_ = wordlist::english.get_word(i);
break; break;
} }
message_[flag_word] -= coin_flag; message_[check_digits] -= coin_flag;
} }
assert(!correction_.empty()); assert(!correction_.empty());
} }
else { else {
message_[flag_word] += coin_flag; message_[check_digits] += coin_flag;
if (!rs.check(message_)) { if (!rs.check(message_)) {
THROW_EXCEPTION("phrase is invalid (checksum mismatch)"); THROW_EXCEPTION("phrase is invalid (checksum mismatch)");
} }
@ -209,12 +191,10 @@ monero_seed::monero_seed(const std::string& phrase, const std::string& coin) {
unsigned used_bits = checksum_size; unsigned used_bits = checksum_size;
unsigned quantized_date; unsigned quantized_date;
net_type_ = 0;
reserved_ = 0; reserved_ = 0;
quantized_date = 0; quantized_date = 0;
memset(seed_.data(), 0, seed_.size()); memset(seed_.data(), 0, seed_.size());
read_data(message_, used_bits, net_type_, net_bits);
read_data(message_, used_bits, reserved_, reserved_bits); read_data(message_, used_bits, reserved_, reserved_bits);
read_data(message_, used_bits, quantized_date, date_bits); read_data(message_, used_bits, quantized_date, date_bits);
@ -228,16 +208,10 @@ monero_seed::monero_seed(const std::string& phrase, const std::string& coin) {
THROW_EXCEPTION("reserved bits must be zero"); THROW_EXCEPTION("reserved bits must be zero");
} }
net_name_ = net_types[net_type_];
if (net_name_ == nullptr) {
THROW_EXCEPTION("invalid network type");
}
date_ = epoch + quantized_date * time_step; date_ = epoch + quantized_date * time_step;
uint8_t salt[25] = "Monero 14-word seed"; uint8_t salt[25] = "Monero 14-word seed";
salt[20] = net_type_; salt[20] = reserved_;
store32(salt + 21, quantized_date); store32(salt + 21, quantized_date);
//argon2id_hash_raw(argon_tcost, argon_mcost, 1, seed_.data(), seed_.size(), salt, sizeof(salt), key_.data(), key_.size()); //argon2id_hash_raw(argon_tcost, argon_mcost, 1, seed_.data(), seed_.size(), salt, sizeof(salt), key_.data(), key_.size());
pbkdf2_hmac_sha256(seed_.data(), seed_.size(), salt, sizeof(salt), pbkdf2_iterations, key_.data(), key_.size()); pbkdf2_hmac_sha256(seed_.data(), seed_.size(), salt, sizeof(salt), pbkdf2_iterations, key_.data(), key_.size());

@ -19,16 +19,13 @@ public:
using secret_key = std::array<uint8_t, key_size>; using secret_key = std::array<uint8_t, key_size>;
using secret_seed = std::array<uint8_t, size>; using secret_seed = std::array<uint8_t, size>;
monero_seed(const std::string& phrase, const std::string& coin); monero_seed(const std::string& phrase, const std::string& coin);
monero_seed(std::time_t date_created, const std::string& coin, const std::string& net); monero_seed(std::time_t date_created, const std::string& coin);
std::time_t date() const { std::time_t date() const {
return date_; return date_;
} }
const std::string& correction() const { const std::string& correction() const {
return correction_; return correction_;
} }
const char* net_name() const {
return net_name_;
}
const secret_key& key() const { const secret_key& key() const {
return key_; return key_;
} }
@ -37,11 +34,9 @@ private:
secret_seed seed_; secret_seed seed_;
secret_key key_; secret_key key_;
std::time_t date_; std::time_t date_;
unsigned net_type_;
unsigned reserved_; unsigned reserved_;
std::string correction_; std::string correction_;
gf_poly message_; gf_poly message_;
const char* net_name_;
}; };
std::ostream& operator<<(std::ostream& os, const monero_seed::secret_key& key); std::ostream& operator<<(std::ostream& os, const monero_seed::secret_key& key);

Loading…
Cancel
Save