Files
thehub/libs/apputils/Mnemonic.cpp
T
2023-11-24 22:20:40 +01:00

225 lines
7.9 KiB
C++

/*
* This file is part of the Flowee project
* Copyright (C) 2021 Tom Zander <tom@flowee.org>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "Mnemonic.h"
#include <hmac_sha512.h>
#include <sha256.h>
#include <Logger.h>
#include <QFile>
#include <string_view>
namespace {
int findOn(const QStringList &haystack, const QString &needle)
{
// TODO replace with binary search
return haystack.indexOf(needle);
}
inline uint8_t bip39Shift(uint32_t bit)
{
return (1 << (8 - (bit % 8) - 1));
}
}
int Mnemonic::findWord(const QString &word)
{
if (!m_words.isEmpty())
return findOn(m_words, word);
QMapIterator<QString, QString> iter(m_wordLists);
while (iter.hasNext()) {
iter.next();
QFile file(iter.value());
if (file.open(QIODevice::ReadOnly)) {
auto txt = QString::fromUtf8(file.readAll());
auto words = txt.split('\n', Qt::SkipEmptyParts);
assert(words.size() == 2048); // deployment error
if (words.size() != 2048)
throw std::runtime_error("Invalid mnemonics lexicon");
int rc = findOn(words, word);
if (rc >= 0) {
m_words = words;
m_languageId = iter.key();
return rc;
}
}
else {
logWarning(Log::Bitcoin) << "Mnemonic: No lexicon found at" << iter.value() << "id:" << iter.key();
}
}
return -1;
}
void Mnemonic::clearSelectedLanguage()
{
m_languageId.clear();
m_words.clear();
}
void Mnemonic::registerWordList(const QString &id, const QString &filename)
{
m_wordLists.insert(id, filename);
}
static constexpr int BitsPerWord = 11;
static constexpr int EntropyBitDivisor = 32;
static constexpr int MnemonicWordMultiple = 3;
Mnemonic::Validity Mnemonic::validateMnemonic(const QString &mnemonic, int &errorIndex, bool *maybeElectrum)
{
if (maybeElectrum)
*maybeElectrum = false; // clear out param, if any
// start with some sanity checks
if (mnemonic.isEmpty())
return IncorrectWordCount;
if (mnemonic.at(0).isSpace() || mnemonic.back().isSpace()) // caller should chomp
return WhitespaceError;
// can't verify until all words are present.
auto words = mnemonic.split(' ', Qt::SkipEmptyParts);
if (words.length() == 1)
words = mnemonic.split(QChar(0x3000), Qt::SkipEmptyParts); // IDEOGRAPHIC SPACE
if (words.length() < 12 || words.length() > 24 || words.length() % MnemonicWordMultiple != 0)
return IncorrectWordCount;
const uint32_t totalBits = BitsPerWord * words.length();
const uint32_t checkBits = totalBits / (EntropyBitDivisor + 1);
const uint32_t entropyBits = totalBits - checkBits;
assert(entropyBits % 8 == 0);
std::vector<uint8_t> data(entropyBits / 8 + 1); // the +1 fits the checksum
uint32_t bit = 0;
errorIndex = 0;
for (const QString &word : std::as_const(words)) {
const auto index = findWord(word);
if (index == -1) {
if (m_languageId.isEmpty())
return UnknownLanguage;
return UnknownWord;
}
errorIndex += word.length() + 1;
assert(index >= 0);
uint32_t wordPos = static_cast<uint32_t>(index);
for (size_t loop = 0; loop < BitsPerWord; ++loop, ++bit) {
if (wordPos & (1 << (BitsPerWord - loop - 1))) {
assert(bit <= totalBits); // don't extend beyond range of vector.
data[bit / 8] |= bip39Shift(bit);
}
}
}
errorIndex = -1;
// calc checksum
const uint8_t checksum = data.back(); // remember checksum
data.resize(data.size() - 1); // cut off the checksum bits (between 3 and 8 bits)
CSHA256 hasher;
hasher.write(data.data(), data.size());
unsigned char buf[CSHA256::OUTPUT_SIZE];
hasher.finalize(buf);
uint8_t mask = 0;
for (uint32_t i = 0; i < checkBits; ++i) { // build mask by adding one bit at a time, high bit first
mask = mask >> 1;
mask += 0x80;
}
assert(checksum == (checksum & mask));
const Validity ret = (buf[0] & mask) != checksum ? ChecksumFailure : Valid;
if (maybeElectrum)
*maybeElectrum = maybeValidElectrumMnemonic(mnemonic);
return ret;
}
bool Mnemonic::maybeValidElectrumMnemonic(const QString &mnemonic) const
{
// Try the phrase versus the Electrum-style checksum check
// The way it works is:
// 1. Initialize an hmac sha512 with "Seed version" as key,
// 2. then write the raw utf-8 encoded bytes of the mnemonic phrase to this hasher,
// 3. and finally, compare the digest to the byte 0x01 (seed version == 0x01).
constexpr std::string_view key = "Seed version";
CHMAC_SHA512 hmac(reinterpret_cast<const unsigned char *>(key.data()), key.size());
const auto mnemonicBytes = mnemonic.toUtf8();
hmac.write(reinterpret_cast<const unsigned char *>(mnemonicBytes.data()), mnemonicBytes.size());
unsigned char digest[CHMAC_SHA512::OUTPUT_SIZE];
hmac.finalize(digest);
// The seed version will always be 0x01, no other versions were ever created.
constexpr unsigned char seedVersion = 0x01u;
return digest[0] == seedVersion;
}
QString Mnemonic::generateMnemonic(const std::vector<uint8_t> &entropy, const QString &langId) const
{
if ((entropy.size() % 4) != 0)
throw std::runtime_error("Entropy sizing invalid; multiple of 4 bytes expected");
QStringList lexicon;
if (m_wordLists.contains(langId)) {
QFile file(m_wordLists[langId]);
if (file.open(QIODevice::ReadOnly)) {
auto txt = QString::fromUtf8(file.readAll());
lexicon= txt.split('\n', Qt::SkipEmptyParts);
assert(lexicon.size() == 2048); // deployment error
if (lexicon.size() != 2048)
throw std::runtime_error("Invalid mnemonics lexicon");
}
else {
logWarning(Log::Bitcoin) << "Mnemonic: No lexicon found at" << m_wordLists.value(langId) << "id:" << langId;
throw std::runtime_error("Missing mnemonics lexicon");
}
}
else {
logWarning() << "Unknown language ID requested" << langId << "available are" << m_wordLists.keys();
throw std::runtime_error("Unknown language Id");
}
const int entropyBits = (entropy.size() * 8);
const int checkBits = (entropyBits / EntropyBitDivisor);
const int totalBits = (entropyBits + checkBits);
const int wordCount = (totalBits / BitsPerWord);
assert((totalBits % BitsPerWord) == 0);
assert((wordCount % MnemonicWordMultiple) == 0);
CSHA256 hasher;
hasher.write(entropy.data(), entropy.size());
std::vector<uint8_t> data(entropy);
data.resize(entropy.size() + CSHA256::OUTPUT_SIZE); // make space
hasher.finalize(&data[entropy.size()]); // append
size_t bit = 0;
QStringList words;
for (int word = 0; word < wordCount; word++) {
size_t position = 0;
for (size_t loop = 0; loop < BitsPerWord; loop++) {
bit = (word * BitsPerWord + loop);
position <<= 1;
const auto byte = bit / 8;
if ((data[byte] & bip39Shift(bit)) > 0)
position++;
}
assert(position < (size_t) lexicon.size());
words.push_back(lexicon[position]);
}
assert(words.size() == ((bit + 1) / BitsPerWord));
return words.join(" ");
}