#include "signing_key.h" #include #include #include #include "json.hpp" #include "sodium_helpers.h" namespace BareMinimumCrypto { using nlohmann::json; namespace fs = std::filesystem; string get_machine_id () { ifstream f; f.open ("/etc/machine-id", ifstream::binary); string machine_id; if (! f.is_open ()) { return machine_id; } f >> machine_id; return machine_id; } Bytes HumanKeyFile::to_msgpack () const { const auto j = json { // Breaking changes should generate a new Base32 schema. {"schema", "3T6XF5DZ"}, {"salt", json::binary (salt)}, {"time_created", time_created.x}, {"pubkey", json::binary (pubkey)}, {"machine_id", machine_id}, }; return json::to_msgpack (j); } optional HumanKeyFile::try_from_msgpack (const json & j) { if (j ["schema"] != "3T6XF5DZ") { return nullopt; } return HumanKeyFile { j ["salt"].get_binary (), Instant (j ["time_created"]), j ["pubkey"].get_binary (), j ["machine_id"], }; } Bytes MachineKeyFile::to_msgpack () const { const auto j = json { // Breaking changes should generate a new Base32 schema. {"schema", "2PVHIKMA"}, {"secretkey", json::binary (secretkey)}, {"time_created", time_created.x}, {"machine_id", machine_id}, }; return json::to_msgpack (j); } optional MachineKeyFile::try_from_msgpack (const json & j) { if (j ["schema"] != "2PVHIKMA") { return nullopt; } return MachineKeyFile { j ["secretkey"].get_binary (), Instant (j ["time_created"]), j ["machine_id"], }; } Bytes MachineKeyFile::pubkey () const { Bytes pk; pk.resize (crypto_sign_PUBLICKEYBYTES); crypto_sign_ed25519_sk_to_pk (pk.data (), secretkey.data ()); return pk; } bool save_key_file (const string & file_path, const Bytes msg) { ofstream f; f.open (file_path, ofstream::binary); if (! f.is_open ()) { return false; } // Best-effort. It probably fails on Windows. fs::permissions (file_path, fs::perms::owner_read, fs::perm_options::replace ); f.write ((const char *)msg.data (), msg.size ()); f.close (); return true; } string get_passphrase_from_user () { cout << "Type or paste passphrase (it will be visible in the console)" << endl; string passphrase; cin >> passphrase; return passphrase; } optional try_load_msgpack_file (const string & file_path) { ifstream f; f.open (file_path, ifstream::binary); if (! f.is_open ()) { return nullopt; } f.seekg (0, ifstream::end); const auto len = f.tellg (); f.seekg (0, ifstream::beg); if (len > 1024 * 1024) { return nullopt; } Bytes bytes; bytes.resize (len); f.read ((char *)bytes.data (), bytes.size ()); return json::from_msgpack (bytes); } optional HumanKeyFile::unlock_key (const Bytes & salt, const string & passphrase) { Bytes seed; seed.resize (crypto_sign_SEEDBYTES); if (crypto_pwhash ( seed.data (), seed.size (), passphrase.data (), passphrase.size (), salt.data (), crypto_pwhash_OPSLIMIT_INTERACTIVE, crypto_pwhash_MEMLIMIT_INTERACTIVE, crypto_pwhash_ALG_DEFAULT ) != 0) { return nullopt; } // This generates a redundant key but that's fine. SigningKey key; //key.pk.resize (crypto_sign_PUBLICKEYBYTES); key.sk.resize (crypto_sign_SECRETKEYBYTES); Bytes pk; pk.resize (crypto_sign_PUBLICKEYBYTES); if (crypto_sign_seed_keypair (pk.data (), key.sk.data (), seed.data ()) != 0) { return nullopt; } return key; } // The whole process for a passphrased key is like this: // Passphrase + random salt --> // Seed --> // Secret key + Public key // Passphrases should be mandatory for keys that can sign other keys. optional HumanKeyFile::generate (const string & file_path, const string & passphrase) { try_sodium_init (); if (passphrase.size () < 8) { return nullopt; } Bytes salt; salt.resize (crypto_pwhash_SALTBYTES); randombytes_buf (salt.data (), salt.size ()); auto key_opt = unlock_key (salt, passphrase); if (! key_opt) { return nullopt; } const auto key = std::move (*key_opt); const auto machine_id = get_machine_id (); HumanKeyFile key_on_disk { salt, Instant::now (), key.pubkey (), machine_id, }; const auto msg = key_on_disk.to_msgpack (); if (! save_key_file (file_path, msg)) { return nullopt; } return key; } optional HumanKeyFile::load (const string & file_path, const string & passphrase) { const auto j = std::move (*try_load_msgpack_file (file_path)); const auto human_key = std::move (*HumanKeyFile::try_from_msgpack (j)); const auto key = std::move (*unlock_key (human_key.salt, passphrase)); return key; } optional MachineKeyFile::generate (const string & file_path) { const SigningKey key; const auto machine_id = get_machine_id (); MachineKeyFile key_on_disk { key.sk, Instant::now (), machine_id, }; const auto msg = key_on_disk.to_msgpack (); if (! save_key_file (file_path, msg)) { return nullopt; } return key; } SigningKey::SigningKey () { try_sodium_init (); Bytes pk; pk.resize (crypto_sign_PUBLICKEYBYTES); sk.resize (crypto_sign_SECRETKEYBYTES); crypto_sign_keypair (pk.data (), sk.data ()); } Bytes SigningKey::pubkey () const { Bytes pk; pk.resize (crypto_sign_PUBLICKEYBYTES); crypto_sign_ed25519_sk_to_pk (pk.data (), sk.data ()); return pk; } Bytes SigningKey::pub_to_msgpack () const { const json j = { {"key", json::binary (pubkey ())}, }; return json::to_msgpack (j); } optional SigningKey::sign ( const Bytes & payload, TimeRange tr ) const { try_sodium_init (); if (tr.duration () > about_1_year) { return nullopt; } const json j { {"not_before", tr.not_before}, {"not_after", tr.not_after}, {"payload", json::binary (payload)}, }; const auto cert = json::to_msgpack (j); Bytes sig; sig.resize (crypto_sign_BYTES); crypto_sign_detached (sig.data (), nullptr, cert.data (), cert.size (), sk.data ()); return ExpiringSignature { cert, sig, }; } optional SigningKey::sign_key (const SigningKey & k, Instant now) const { return sign (k.pub_to_msgpack (), TimeRange::from_start_and_dur (now, about_3_months)); } optional SigningKey::sign_data (const Bytes & v, Instant now) const { return sign (v, TimeRange::from_start_and_dur (now, about_1_week)); } }