add network type

reserved bits: 2 -> 3
This commit is contained in:
tevador 2020-06-15 22:26:29 +02:00
parent f1c7829f04
commit 41546fa019
5 changed files with 186 additions and 69 deletions

100
README.md
View File

@ -7,36 +7,61 @@ cmake ..
make make
``` ```
## Features
* 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
* 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
## Usage ## Usage
### Create a new seed ### Create a new seed
The sole argument is the wallet creation date in `yyyy/MM/dd` format.
``` ```
> ./monero-seed --create 2100/03/14 > ./monero-seed --create [--coin <monero|aeon>] [--net <MAIN|STAGE|TEST>] [--date <yyyy-MM-dd>]
Mnemonic phrase: pumpkin alter spice lend position sentence surface snow atom lobster exotic robot profit chase ```
- version: 1
- private key: 9a1a9fe303f84d39277c0e87ccf42aa78f19c28127b2187d574266f29992971f Example:
```
> ./monero-seed --create --coin monero --net MAIN --date 2100/03/14
Mnemonic phrase: test park taste security oxygen decorate essence ridge ship fish vehicle dream fluid pattern
- coin: monero
- network: MAIN
- private key: 7b816d8134e29393b0333eed4b6ed6edf97c156ad139055a706a6fb9599dcf8c
- created on or after: 02/Mar/2100 - created on or after: 02/Mar/2100
``` ```
### Restore seed ### Restore seed
```
./monero-seed --restore "<14-word seed>" [--coin <monero|aeon>]
```
Example:
``` ```
> ./monero-seed --restore "pumpkin alter spice lend position sentence surface snow atom lobster exotic robot profit chase" > ./monero-seed --restore "test park taste security oxygen decorate essence ridge ship fish vehicle dream fluid pattern" --coin monero
- version: 1 - coin: monero
- private key: 9a1a9fe303f84d39277c0e87ccf42aa78f19c28127b2187d574266f29992971f - network: MAIN
- private key: 7b816d8134e29393b0333eed4b6ed6edf97c156ad139055a706a6fb9599dcf8c
- created on or after: 02/Mar/2100 - created on or after: 02/Mar/2100
``` ```
Restore has limited error correction capability, namely it can correct a single erasure (illegible symbol with a known location). Attempting to restore the same seed under a different coin will fail:
```
> ./monero-seed --restore "test park taste security oxygen decorate essence ridge ship fish vehicle dream fluid pattern" --coin aeon
ERROR: phrase is invalid (checksum mismatch)
```
Restore has limited error correction capability, namely it can correct a single erasure (illegible word with a known location).
This can be tested by replacing a word with `xxxx`: This can be tested by replacing a word with `xxxx`:
``` ```
> ./monero-seed --restore "pumpkin alter xxxx lend position sentence surface snow atom lobster exotic robot profit chase" > ./monero-seed --restore "test park xxxx security oxygen decorate essence ridge ship fish vehicle dream fluid pattern" --coin monero
Warning: corrected erasure: xxxx -> spice Warning: corrected erasure: xxxx -> taste
- version: 1 - coin: monero
- private key: 9a1a9fe303f84d39277c0e87ccf42aa78f19c28127b2187d574266f29992971f - network: MAIN
- private key: 7b816d8134e29393b0333eed4b6ed6edf97c156ad139055a706a6fb9599dcf8c
- created on or after: 02/Mar/2100 - created on or after: 02/Mar/2100
``` ```
@ -44,35 +69,52 @@ Warning: corrected erasure: xxxx -> spice
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:
* 3 bits for version (this allows the format to be updated up to 7 times) * 2 bits for the network type
* 2 bits reserved for future use * 3 bits reserved for future use
* 10 bits for approximate wallet creation date * 10 bits for approximate wallet birthday
* 128 bits for the private key seed * 128 bits for the private key seed
* 11 bits for error detection/correction * 11 bits for checksum
### Wordlist ### Wordlist
Uses the wordlist from BIP-39. It 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. 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).
### Wallet creation date ### Network type
The mnemonic phrase doesn't store block height, but the time when the wallet was created. This allows the seed to be generated The network type is stored in 2 bits as follows:
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.
### Private key seed * `00` = mainnet
* `01` = stagenet
* `10` = testnet
* `11` = invalid
PBKDF2 with 4096 iterations is used to generate the private key from the 128-bit seed included in the mnemonic phrase. The wallet creation date is used as a salt. 128-bit seed provides the same level of security as the elliptic curve used by Monero. 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 2 reserved bits for future use. Possible use cases: 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.
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
### Error detection/correction ### Wallet birthday
The mnemonic phrase can be treated as a polynomial over GF(2048), which allows us to use an efficient Reed-Solomon ECC with one check word. All single-word errors can be detected and all single-word erasures can be corrected. 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 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.
### 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.
Future extensions may define other KDFs.
### Checksum
The mnemonic phrase can be treated as a polynomial over GF(2048), which allows us to use an efficient Reed-Solomon error correction code with one check word. All single-word errors can be detected and all single-word erasures can be corrected without false positives.
To prevent the seed from being accidentally used with a different cryptocurrency, a coin-specific value is subtracted from the first data-word after the checksum is calculated. Checksum validation will fail unless the wallet software adds the same value back to the first data-word when restoring.

View File

@ -11,10 +11,10 @@ public:
static constexpr gf_item size() { static constexpr gf_item size() {
return gf_2048::size(); return gf_2048::size();
} }
gf_elem() : value_(0) constexpr gf_elem() : value_(0)
{ {
} }
gf_elem(gf_item value) : value_(value) constexpr gf_elem(gf_item value) : value_(value)
{ {
} }
gf_elem& operator+=(gf_elem x) { gf_elem& operator+=(gf_elem x) {

View File

@ -10,14 +10,26 @@
#include <stdexcept> #include <stdexcept>
#include <cstring> #include <cstring>
static inline void read_string_option(const char* option, int argc, char** argv, char** out) { static inline void read_string_option(const char* option, int argc,
const char** argv, const char** out, const char* def_val = nullptr) {
for (int i = 0; i < argc - 1; ++i) { for (int i = 0; i < argc - 1; ++i) {
if (strcmp(argv[i], option) == 0) { if (strcmp(argv[i], option) == 0) {
*out = argv[i + 1]; *out = argv[i + 1];
return; return;
} }
} }
*out = NULL; *out = def_val;
}
static inline void read_option(const char* option, int argc, const char** argv,
bool& out) {
for (int i = 0; i < argc; ++i) {
if (strcmp(argv[i], option) == 0) {
out = true;
return;
}
}
out = false;
} }
static time_t parse_date(const char* s) { static time_t parse_date(const char* s) {
@ -39,40 +51,54 @@ static time_t parse_date(const char* s) {
throw std::runtime_error("invalid date"); throw std::runtime_error("invalid date");
} }
void print_seed(const monero_seed& seed, bool phrase) { void print_seed(const monero_seed& seed, const char* coin, bool phrase) {
if (!seed.correction().empty()) { if (!seed.correction().empty()) {
std::cout << "Warning: corrected erasure: " << monero_seed::erasure << " -> " << seed.correction() << std::endl; std::cout << "Warning: corrected erasure: " << monero_seed::erasure << " -> " << seed.correction() << std::endl;
} }
if (phrase) { if (phrase) {
std::cout << "Mnemonic phrase: " << seed << std::endl; std::cout << "Mnemonic phrase: " << seed << std::endl;
} }
std::cout << "- version: " << seed.version() << 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);
std::cout << "- created on or after: " << std::put_time(&tm, "%d/%b/%Y") << std::endl; std::cout << "- created on or after: " << std::put_time(&tm, "%d/%b/%Y") << std::endl;
} }
int main(int argc, char** argv) { int main(int argc, const char* argv[]) {
char* create; bool create;
char* restore; const char* create_date;
read_string_option("--create", argc, argv, &create); const char* create_net;
const char* coin;
const char* restore;
read_option("--create", argc, argv, create);
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("--restore", argc, argv, &restore); read_string_option("--restore", argc, argv, &restore);
try { try {
if (create != NULL) { if (create) {
monero_seed seed(parse_date(create)); time_t time;
print_seed(seed, true); if (create_date != nullptr) {
time = parse_date(create_date);
} }
else if (restore != NULL) { else {
monero_seed seed(restore); time = std::time(nullptr);
print_seed(seed, false); }
monero_seed seed(time, coin, create_net);
print_seed(seed, coin, true);
}
else if (restore != nullptr) {
monero_seed seed(restore, coin);
print_seed(seed, coin, false);
} }
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 <yyyy-MM-dd>" << std::endl; std::cout << argv[0] << " --create [--coin <monero|aeon>] [--net <MAIN|STAGE|TEST>] [--date <yyyy-MM-dd>]" << std::endl;
std::cout << argv[0] << " --restore <14-word seed>" << std::endl; std::cout << argv[0] << " --restore \"<14-word seed>\" [--coin <monero|aeon>]" << std::endl;
} }
} }
catch (const std::exception & ex) { catch (const std::exception & ex) {

View File

@ -47,9 +47,9 @@ 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 version_bits = 3; constexpr unsigned net_bits = 2;
constexpr unsigned version_mask = (1u << version_bits) - 1; constexpr unsigned net_mask = (1u << net_bits) - 1;
constexpr unsigned reserved_bits = 2; 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;
@ -59,8 +59,21 @@ constexpr uint32_t argon_tcost = 3;
constexpr uint32_t argon_mcost = 256 * 1024; constexpr uint32_t argon_mcost = 256 * 1024;
constexpr int pbkdf2_iterations = 4096; constexpr int pbkdf2_iterations = 4096;
static const std::string COIN_MONERO = "monero";
static const std::string COIN_AEON = "aeon";
constexpr gf_elem monero_flag = gf_elem(0x539);
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_assert(total_bits static_assert(total_bits
== version_bits + date_bits + reserved_bits + checksum_size + == net_bits + 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");
@ -85,32 +98,53 @@ static void read_data(gf_poly& poly, unsigned& used_bits, T& value, unsigned bit
unsigned digit_bits = std::min((unsigned)gf_elem::size() - bit_index, bits); unsigned digit_bits = std::min((unsigned)gf_elem::size() - bit_index, bits);
unsigned rem_bits = gf_elem::size() - bit_index - digit_bits; unsigned rem_bits = gf_elem::size() - bit_index - digit_bits;
unsigned rest_bits = bits - digit_bits; unsigned rest_bits = bits - digit_bits;
value |= ((poly[coeff_index].value() >> rem_bits) & ((1u << bits) - 1)) << rest_bits; value |= ((poly[coeff_index].value() >> rem_bits) & ((1u << digit_bits) - 1)) << rest_bits;
used_bits += digit_bits; used_bits += digit_bits;
if (rest_bits > 0) { if (rest_bits > 0) {
read_data(poly, used_bits, value, rest_bits); read_data(poly, used_bits, value, rest_bits);
} }
} }
static gf_elem get_coin_flag(const std::string& coin) {
if (coin == COIN_MONERO) {
return monero_flag;
}
else if (coin == COIN_AEON) {
return aeon_flag;
}
else {
THROW_EXCEPTION("invalid 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) { monero_seed::monero_seed(std::time_t date_created, const std::string& coin, const std::string& net) {
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;
version_ = 0; 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] = version_; salt[20] = net_type_;
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, version_, version_bits); 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_) {
@ -118,9 +152,11 @@ monero_seed::monero_seed(std::time_t date_created) {
} }
assert(rem_bits == 0); assert(rem_bits == 0);
rs.encode(message_); rs.encode(message_);
message_[flag_word] -= coin_flag;
} }
monero_seed::monero_seed(const std::string& phrase) { monero_seed::monero_seed(const std::string& phrase, const std::string& coin) {
gf_elem coin_flag = get_coin_flag(coin);
int word_count = 0; int word_count = 0;
size_t offset = 0; size_t offset = 0;
int error = -1; int error = -1;
@ -137,7 +173,7 @@ monero_seed::monero_seed(const std::string& phrase) {
THROW_EXCEPTION("unrecognized word: '" << words[word_count] << "'"); THROW_EXCEPTION("unrecognized word: '" << words[word_count] << "'");
} }
if (error >= 0) { if (error >= 0) {
THROW_EXCEPTION("teo or more erasures cannot be corrected"); THROW_EXCEPTION("two or more erasures cannot be corrected");
} }
error = word_count; error = word_count;
} }
@ -155,14 +191,17 @@ monero_seed::monero_seed(const std::string& phrase) {
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;
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;
} }
assert(!correction_.empty()); assert(!correction_.empty());
} }
else { else {
message_[flag_word] += coin_flag;
if (!rs.check(message_)) { if (!rs.check(message_)) {
THROW_EXCEPTION("phrase is invalid (checksum mismatch)"); THROW_EXCEPTION("phrase is invalid (checksum mismatch)");
} }
@ -170,13 +209,12 @@ monero_seed::monero_seed(const std::string& phrase) {
unsigned used_bits = checksum_size; unsigned used_bits = checksum_size;
unsigned quantized_date; unsigned quantized_date;
net_type_ = 0;
version_ = 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, version_, version_bits); 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);
@ -186,10 +224,20 @@ monero_seed::monero_seed(const std::string& phrase) {
assert(used_bits == total_bits); assert(used_bits == total_bits);
if (reserved_ != 0) {
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] = version_; salt[20] = net_type_;
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());

View File

@ -18,17 +18,17 @@ public:
static constexpr size_t key_size = 32; static constexpr size_t key_size = 32;
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); monero_seed(const std::string& phrase, const std::string& coin);
monero_seed(std::time_t date_created); monero_seed(std::time_t date_created, const std::string& coin, const std::string& net);
std::time_t date() const { std::time_t date() const {
return date_; return date_;
} }
unsigned version() const {
return version_ + 1;
}
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,10 +37,11 @@ private:
secret_seed seed_; secret_seed seed_;
secret_key key_; secret_key key_;
std::time_t date_; std::time_t date_;
unsigned version_; 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);