oxen-core/src/blockchain_utilities/sn_key_tool.cpp

371 lines
15 KiB
C++

#include <algorithm>
extern "C" {
#include <sodium.h>
}
#include <iostream>
#include <fstream>
#include <lokimq/hex.h>
#include <lokimq/base32z.h>
#include <string_view>
#include <string>
#include <list>
#include <array>
#include <cstring>
#include <optional>
#include "common/fs.h"
std::string_view arg0;
using namespace std::literals;
int usage(int exit_code, std::string_view msg = ""sv) {
if (!msg.empty())
std::cout << "\n" << msg << "\n\n";
std::cout << "Usage: " << arg0 << R"( COMMAND [OPTIONS...] where support COMMANDs are:
generate [--overwrite] FILENAME
Generates a new Ed25519 service node keypair and writes the secret key to
FILENAME. If FILENAME contains the string "PUBKEY" it will be replaced
with the generated public key value (in hex).
For an active service node this file is named `key_ed25519` in the oxend
data directory.
If FILENAME already exists the command will fail unless the `--overwrite`
flag is specified.
legacy [--overwrite] FILENAME
Generates a new service node legacy keypair and write the private key to
FILENAME. If FILENAME contains the string "PUBKEY" it will be replaced with
the generated public key value (in hex).
If FILENAME already exists the command will fail unless the `--overwrite`
flag is specified.
Note that legacy keypairs are not needed as of Oxen 8.x; you can use just a
Ed25519 keypair (and this is the default for new service node
installations).
show [--ed25519|--legacy] FILENAME
Reads FILENAME as a service node secret key (Ed25519 or legacy) and
displays it as a hex value along with the associated public key. The
displayed secret key can be saved and later used to recreate the secret key
file with the `restore` command.
--ed25519 and --legacy are not normally required as they can usually be
inferred from the size of the given file (32 bytes = legacy, 64 bytes =
Ed25519). The options can be used to force the file to be interpreted as
a secret key of the specified type.
restore [--overwrite] FILENAME
restore-legacy [--overwrite] FILENAME
Restore an Ed25519 (restore) or legacy (restore-legacy) secret key and
write it to FILENAME. You will be prompted to provide a secret key hex
value (as produced by the show command) and asked to confirm the public key
for confirmation. As with `generate', if FILENAME contains the string
"PUBKEY" it will be replaced with the actual public key (in hex).
If FILENAME already exists the command will fail unless the `--overwrite`
flag is specified.
)";
return exit_code;
}
[[nodiscard]] int error(int exit_code, std::string_view msg) {
std::cout << "\n" << msg << "\n\n";
return exit_code;
}
using ustring = std::basic_string<unsigned char>;
using ustring_view = std::basic_string_view<unsigned char>;
std::array<unsigned char, crypto_core_ed25519_BYTES> pubkey_from_privkey(ustring_view privkey) {
std::array<unsigned char, crypto_core_ed25519_BYTES> pubkey;
// noclamp because Monero keys are not clamped at all, and because sodium keys are pre-clamped.
crypto_scalarmult_ed25519_base_noclamp(pubkey.data(), privkey.data());
return pubkey;
}
template <size_t N, std::enable_if_t<(N >= 32), int> = 0>
std::array<unsigned char, crypto_core_ed25519_BYTES> pubkey_from_privkey(const std::array<unsigned char, N>& privkey) {
return pubkey_from_privkey(ustring_view{privkey.data(), 32});
}
int generate(bool ed25519, std::list<std::string_view> args) {
bool overwrite = false;
if (!args.empty()) {
if (args.front() == "--overwrite") {
overwrite = true;
args.pop_front();
} else if (args.back() == "--overwrite") {
overwrite = true;
args.pop_back();
}
}
if (args.empty())
return error(2, "generate requires a FILENAME");
else if (args.size() > 1)
return error(2, "unknown arguments to 'generate'");
std::string filename{args.front()};
size_t pubkey_pos = filename.find("PUBKEY");
if (pubkey_pos != std::string::npos)
overwrite = true;
if (!overwrite && fs::exists(fs::u8path(filename)))
return error(2, filename + " to generate already exists, pass `--overwrite' if you want to overwrite it");
std::array<unsigned char, crypto_sign_PUBLICKEYBYTES> pubkey;
std::array<unsigned char, crypto_sign_SECRETKEYBYTES> seckey;
crypto_sign_keypair(pubkey.data(), seckey.data());
std::array<unsigned char, crypto_hash_sha512_BYTES> privkey_signhash;
crypto_hash_sha512(privkey_signhash.data(), seckey.data(), 32);
// Clamp it to prevent small subgroups:
privkey_signhash[0] &= 248;
privkey_signhash[31] &= 63;
privkey_signhash[31] |= 64;
ustring_view privkey{privkey_signhash.data(), 32};
// Double-check that we did it properly:
if (pubkey_from_privkey(privkey) != pubkey)
return error(11, "Internal error: pubkey check failed");
if (pubkey_pos != std::string::npos)
filename.replace(pubkey_pos, 6, lokimq::to_hex(pubkey.begin(), pubkey.end()));
fs::ofstream out{fs::u8path(filename), std::ios::trunc | std::ios::binary};
if (!out.good())
return error(2, "Failed to open output file '" + filename + "': " + std::strerror(errno));
if (ed25519)
out.write(reinterpret_cast<const char*>(seckey.data()), seckey.size());
else
out.write(reinterpret_cast<const char*>(privkey.data()), privkey.size());
if (!out.good())
return error(2, "Failed to write to output file '" + filename + "': " + std::strerror(errno));
std::cout << "Generated SN " << (ed25519 ? "Ed25519 secret key" : "legacy private key") << " in " << filename << "\n";
if (ed25519) {
std::array<unsigned char, crypto_scalarmult_curve25519_BYTES> x_pubkey;
if (0 != crypto_sign_ed25519_pk_to_curve25519(x_pubkey.data(), pubkey.data()))
return error(14, "Internal error: unable to convert Ed25519 pubkey to X25519 pubkey");
std::cout <<
"Public key: " << lokimq::to_hex(pubkey.begin(), pubkey.end()) <<
"\nX25519 pubkey: " << lokimq::to_hex(x_pubkey.begin(), x_pubkey.end()) <<
"\nLokinet address: " << lokimq::to_base32z(pubkey.begin(), pubkey.end()) << ".snode\n";
} else {
std::cout << "Public key: " << lokimq::to_hex(pubkey.begin(), pubkey.end()) << "\n";
}
return 0;
}
int show(std::list<std::string_view> args) {
bool legacy = false, ed25519 = false;
if (!args.empty()) {
if (args.front() == "--legacy") {
legacy = true;
args.pop_front();
} else if (args.back() == "--legacy") {
legacy = true;
args.pop_back();
} else if (args.front() == "--ed25519") {
ed25519 = true;
args.pop_front();
} else if (args.back() == "--ed25519") {
ed25519 = true;
args.pop_back();
}
}
if (args.empty())
return error(2, "show requires a FILENAME");
else if (args.size() > 1)
return error(2, "unknown arguments to 'show'");
fs::path filename = fs::u8path(args.front());
fs::ifstream in{filename, std::ios::binary};
if (!in.good())
return error(2, "Unable to open '" + filename.u8string() + "': " + std::strerror(errno));
in.seekg(0, std::ios::end);
auto size = in.tellg();
in.seekg(0, std::ios::beg);
if (!legacy && !ed25519) {
if (size == 32)
legacy = true;
else if (size == 64)
ed25519 = true;
}
if (!legacy && !ed25519)
return error(2, "Could not autodetect key type from " + std::to_string(size) + "-byte file; check the file or pass the --ed25519 or --legacy argument");
if (size < 32)
return error(2, "File size (" + std::to_string(size) + " bytes) is too small to be a secret key");
std::array<unsigned char, crypto_core_ed25519_BYTES> pubkey;
std::array<unsigned char, crypto_scalarmult_curve25519_BYTES> x_pubkey;
std::array<unsigned char, crypto_sign_SECRETKEYBYTES> seckey;
in.read(reinterpret_cast<char*>(seckey.data()), size >= 64 ? 64 : 32);
if (!in.good())
return error(2, "Failed to read from " + filename.u8string() + ": " + std::strerror(errno));
if (legacy) {
pubkey = pubkey_from_privkey(seckey);
std::cout << filename.u8string() << " (legacy SN keypair)" << "\n==========" <<
"\nPrivate key: " << lokimq::to_hex(seckey.begin(), seckey.begin() + 32) <<
"\nPublic key: " << lokimq::to_hex(pubkey.begin(), pubkey.end()) << "\n\n";
return 0;
}
std::array<unsigned char, crypto_hash_sha512_BYTES> privkey_signhash;
crypto_hash_sha512(privkey_signhash.data(), seckey.data(), 32);
privkey_signhash[0] &= 248;
privkey_signhash[31] &= 63;
privkey_signhash[31] |= 64;
ustring_view privkey{privkey_signhash.data(), 32};
pubkey = pubkey_from_privkey(privkey);
if (size >= 64 && ustring_view{pubkey.data(), pubkey.size()} != ustring_view{seckey.data() + 32, 32})
return error(13, "Error: derived pubkey (" + lokimq::to_hex(pubkey.begin(), pubkey.end()) + ")"
" != embedded pubkey (" + lokimq::to_hex(seckey.begin() + 32, seckey.end()) + ")");
if (0 != crypto_sign_ed25519_pk_to_curve25519(x_pubkey.data(), pubkey.data()))
return error(14, "Unable to convert Ed25519 pubkey to X25519 pubkey; is this a really valid secret key?");
std::cout << filename << " (Ed25519 SN keypair)" << "\n==========" <<
"\nSecret key: " << lokimq::to_hex(seckey.begin(), seckey.begin() + 32) <<
"\nPublic key: " << lokimq::to_hex(pubkey.begin(), pubkey.end()) <<
"\nX25519 pubkey: " << lokimq::to_hex(x_pubkey.begin(), x_pubkey.end()) <<
"\nLokinet address: " << lokimq::to_base32z(pubkey.begin(), pubkey.end()) << ".snode\n\n";
return 0;
}
int restore(bool ed25519, std::list<std::string_view> args) {
bool overwrite = false;
if (!args.empty()) {
if (args.front() == "--overwrite") {
overwrite = true;
args.pop_front();
} else if (args.back() == "--overwrite") {
overwrite = true;
args.pop_back();
}
}
if (args.empty())
return error(2, "restore requires a FILENAME");
else if (args.size() > 1)
return error(2, "unknown arguments to 'restore'");
std::string filename{args.front()};
size_t pubkey_pos = filename.find("PUBKEY");
if (ed25519)
std::cout << "Enter the Ed25519 secret key:\n";
else
std::cout << "Enter the legacy SN private key:\n";
char buf[129];
std::cin.getline(buf, 129);
if (!std::cin.good())
return error(7, "Invalid input, aborting!");
std::string_view skey_hex{buf};
// Advanced feature: if you provide the concatenated privkey and pubkey in hex, we won't prompt
// for verification (as long as the pubkey matches what we derive from the privkey).
if (!(skey_hex.size() == 64 || skey_hex.size() == 128) || !lokimq::is_hex(skey_hex))
return error(7, "Invalid input: provide the secret key as 64 hex characters");
std::array<unsigned char, crypto_sign_SECRETKEYBYTES> skey;
std::array<unsigned char, crypto_sign_PUBLICKEYBYTES> pubkey;
std::array<unsigned char, crypto_sign_SEEDBYTES> seed;
std::optional<std::array<unsigned char, crypto_sign_PUBLICKEYBYTES>> pubkey_expected;
lokimq::from_hex(skey_hex.begin(), skey_hex.begin() + 64, seed.begin());
if (skey_hex.size() == 128)
lokimq::from_hex(skey_hex.begin() + 64, skey_hex.end(), pubkey_expected.emplace().begin());
if (ed25519) {
crypto_sign_seed_keypair(pubkey.data(), skey.data(), seed.data());
} else {
pubkey = pubkey_from_privkey(seed);
}
std::cout << "\nPublic key: " << lokimq::to_hex(pubkey.begin(), pubkey.end()) << "\n";
if (ed25519) {
std::array<unsigned char, crypto_scalarmult_curve25519_BYTES> x_pubkey;
if (0 != crypto_sign_ed25519_pk_to_curve25519(x_pubkey.data(), pubkey.data()))
return error(14, "Unable to convert Ed25519 pubkey to X25519 pubkey; is this a really valid secret key?");
std::cout << "X25519 pubkey: " << lokimq::to_hex(x_pubkey.begin(), x_pubkey.end()) <<
"\nLokinet address: " << lokimq::to_base32z(pubkey.begin(), pubkey.end()) << ".snode";
}
if (pubkey_expected) {
if (*pubkey_expected != pubkey)
return error(2, "Derived pubkey (" + lokimq::to_hex(pubkey.begin(), pubkey.end()) + ") doesn't match "
"provided pubkey (" + lokimq::to_hex(pubkey_expected->begin(), pubkey_expected->end()) + ")");
} else {
std::cout << "\nIs this correct? Press Enter to continue, Ctrl-C to cancel.\n";
std::cin.getline(buf, 129);
if (!std::cin.good())
return error(99, "Aborted");
}
if (pubkey_pos != std::string::npos)
filename.replace(pubkey_pos, 6, lokimq::to_hex(pubkey.begin(), pubkey.end()));
auto filepath = fs::u8path(filename);
if (!overwrite && fs::exists(filepath))
return error(2, filename + " to generate already exists, pass `--overwrite' if you want to overwrite it");
fs::ofstream out{filepath, std::ios::trunc | std::ios::binary};
if (!out.good())
return error(2, "Failed to open output file '" + filename + "': " + std::strerror(errno));
if (ed25519)
out.write(reinterpret_cast<const char*>(skey.data()), skey.size());
else
out.write(reinterpret_cast<const char*>(seed.data()), seed.size());
if (!out.good())
return error(2, "Failed to write to output file '" + filename + "': " + std::strerror(errno));
std::cout << "Saved secret key to " << filename << "\n";
return 0;
}
int main(int argc, char* argv[]) {
arg0 = argv[0];
if (argc < 2)
return usage(1, "No command specified!");
std::string_view cmd{argv[1]};
std::list<std::string_view> args{argv + 2, argv + argc};
if (sodium_init() == -1) {
std::cerr << "Sodium initialization failed! Unable to continue.\n\n";
return 3;
}
for (auto& flag : {"--help"sv, "-h"sv, "-?"sv})
for (auto& arg : args)
if (arg == flag)
return usage(0);
if (cmd == "generate")
return generate(true, std::move(args));
if (cmd == "legacy")
return generate(false, std::move(args));
if (cmd == "show")
return show(std::move(args));
if (cmd == "restore")
return restore(true, std::move(args));
if (cmd == "restore-legacy")
return restore(false, std::move(args));
return usage(1, "Unknown command `" + std::string{cmd} + "'");
}