Files
scylladb/ent/encryption/local_file_provider.cc
Kefu Chai 9fdbe0e74b tree: Remove unused boost headers
This commit eliminates unused boost header includes from the tree.

Removing these unnecessary includes reduces dependencies on the
external Boost.Adapters library, leading to faster compile times
and a slightly cleaner codebase.

Signed-off-by: Kefu Chai <kefu.chai@scylladb.com>

Closes scylladb/scylladb#22997
2025-02-25 10:32:32 +03:00

291 lines
10 KiB
C++

/*
* Copyright (C) 2018 ScyllaDB
*
*/
/*
* SPDX-License-Identifier: LicenseRef-ScyllaDB-Source-Available-1.0
*/
#include <unordered_map>
#include <stdexcept>
#include <regex>
#include <openssl/evp.h>
#include <openssl/rand.h>
#include <seastar/core/semaphore.hh>
#include <seastar/core/seastar.hh>
#include <seastar/core/fstream.hh>
#include <seastar/core/reactor.hh>
#include "local_file_provider.hh"
#include "symmetric_key.hh"
#include "encryption.hh"
#include "encryption_exceptions.hh"
#include "encryption_config.hh"
#include "db/config.hh"
namespace encryption {
namespace bfs = std::filesystem;
const sstring default_key_file_path = (bfs::path(db::config::get_conf_dir()) / "data_encryption_keys").string();
static const key_info system_key_info{ "System", 0 };
class local_file_provider : public key_provider {
public:
local_file_provider(encryption_context& ctxt, const bfs::path& path, bool must_exist = false)
: local_file_provider(ctxt, sstring(bfs::absolute(path).string()), must_exist)
{}
local_file_provider(encryption_context& ctxt, const sstring& path, bool must_exist = false)
: _ctxt(ctxt)
, _path(path)
, _sem(1)
, _must_exist(must_exist)
{}
future<std::tuple<key_ptr, opt_bytes>> key(const key_info& info, opt_bytes = {}) override {
// TODO: assert options -> my key
auto i = _keys.find(info);
if (i != _keys.end()) {
return make_ready_future<std::tuple<key_ptr, opt_bytes>>(std::tuple(i->second, std::nullopt));
}
return load_or_create(info).then([](key_ptr k) {
return make_ready_future<std::tuple<key_ptr, opt_bytes>>(std::tuple(k, std::nullopt));
});
}
future<> validate() const override {
auto f = make_ready_future<>();
if (!_must_exist) {
return f;
}
// if we must exist, we don't change. Ok to open from all shards.
return f.then([this] {
return open_file_dma(_path, open_flags::ro).then([](file f) {
return f.close();
});
}).handle_exception([this](auto ep) {
try {
std::rethrow_exception(ep);
} catch (...) {
std::throw_with_nested(missing_resource_error("Could not read '" + _path + "'"));
}
});
}
const sstring& path() const {
return _path;
}
void print(std::ostream& os) const override {
os << "key=" << _path;
}
private:
future<key_ptr> load_or_create(const key_info&);
future<key_ptr> load_or_create_local(const key_info&);
future<> read_key_file();
future<key_ptr> write_key_file(key_info);
std::unordered_map<key_info, key_ptr, key_info_hash> _keys;
encryption_context& _ctxt;
sstring _path;
semaphore _sem;
bool _read_file = false;
bool _must_exist = false;
};
shared_ptr<key_provider> local_file_provider_factory::find(encryption_context& ctxt, const sstring& path) {
auto p = ctxt.get_cached_provider(path);
if (!p) {
p = make_shared<local_file_provider>(ctxt, path);
ctxt.cache_provider(path, p);
}
return p;
}
shared_ptr<key_provider> local_file_provider_factory::get_provider(encryption_context& ctxt, const options& map) {
opt_wrapper opts(map);
return find(ctxt, opts(SECRET_KEY_FILE).value_or(default_key_file_path));
}
future<key_ptr>
local_file_provider::load_or_create(const key_info& info) {
// if someone uses a system key as a table key, we could still race
// here. but that is a user error, so ignore
if (this_shard_id() == 0 || &info == &system_key_info) {
return load_or_create_local(info);
}
struct data {
bytes key;
key_info info;
};
/**
* Key files are singular. Not sharded. This would be ok if we only read from them.
* But in keeping with dse compat, we don't. So rather than dealing with lock files
* or whatnot, we simply say that a single file is handled by a single key object,
* and only on shard 0. So if we are not shard 0, we call to there, find our
* counterpart object (local_file_provider_factory::find), and as him about the
* key data instead. He in turn will sync on his semaphore.
*
* The downside is that we are not resilient against multiple processes messing
* with the key file, but neither is dse
*/
return do_with(data{bytes(bytes::initialized_later(), info.len/8), info}, [this](data& i) {
return smp::submit_to(0, [this, &i]{
auto kp = static_pointer_cast<local_file_provider>(local_file_provider_factory::find(_ctxt, _path));
auto f = kp->load_or_create_local(i.info);
return f.then([&i, kp](key_ptr k) {
auto& kd = k->key();
i.key.resize(kd.size());
std::copy(kd.begin(), kd.end(), i.key.begin());
});
}).then([this, &i] {
auto k = make_shared<symmetric_key>(i.info, i.key);
_keys.emplace(i.info, k);
return make_ready_future<key_ptr>(std::move(k));
});
});
}
future<key_ptr>
local_file_provider::load_or_create_local(const key_info& info) {
if (_keys.count(info)) {
return make_ready_future<key_ptr>(_keys.at(info));
}
return read_key_file().then([this, info] {
if (_keys.count(info)) {
return make_ready_future<key_ptr>(_keys.at(info));
}
if (info == system_key_info) {
if (_keys.size() != 1) {
_keys.clear();
return make_exception_future<key_ptr>(std::invalid_argument("System key must contain exactly one entry"));
}
auto k = _keys.begin()->second;
_keys.clear();
_keys.emplace(info, k);
return make_ready_future<key_ptr>(k);
}
// create it.
return write_key_file(info);
});
}
future<> local_file_provider::read_key_file() {
if (_read_file) {
return make_ready_future();
}
// #1923 - a key can have a descriptor string line "AES:128:<data>" iff user relies on
// defaults. Must match this as well.
static const std::regex key_line_expr(R"foo((\w+(?:\/\w+)?(?:\/\w+)?)\:(\d+)\:(\S+)\s*)foo");
return with_semaphore(_sem, 1, [this] {
// could do this twice, but it is only reading
return read_text_file_fully(_path).then([this](temporary_buffer<char> buf) {
auto i = std::cregex_iterator(buf.begin(), buf.end(), key_line_expr);
auto e = std::cregex_iterator();
while (i != e) {
std::cmatch m = *i;
auto alg = m[1].str();
auto len = std::stoul(m[2].str());
auto key = m[3].str();
auto info = key_info{alg, unsigned(len)};
if (!_keys.count(info)) {
auto kb = base64_decode(key);
auto k = make_shared<symmetric_key>(info, kb);
_keys.emplace(info, std::move(k));
}
++i;
}
_read_file = true;
}).handle_exception([this](auto ep) {
try {
std::rethrow_exception(ep);
} catch (std::system_error& e) {
if (e.code() == std::error_code(ENOENT, std::system_category())) {
if (!_must_exist) {
return;
}
std::throw_with_nested(configuration_error("Key file '" + _path + "' does not exist"));
}
std::throw_with_nested(service_error("read_key_file"));
} catch (std::invalid_argument& e) {
std::throw_with_nested(configuration_error(fmt::format("read_key_file: {}", e.what())));
} catch (...) {
std::throw_with_nested(service_error(fmt::format("read_key_file: {}", std::current_exception())));
}
});
});
}
future<key_ptr> local_file_provider::write_key_file(key_info info) {
return with_semaphore(_sem, 1, [this, info] {
// we can get here more than once if shards race.
// however, we only need to use/write the first key matching
// the required info.
if (_keys.count(info)) {
return make_ready_future<key_ptr>(_keys.at(info));
}
auto k = make_shared<symmetric_key>(info);
std::ostringstream ss;
for (auto& p : _keys) {
ss << p.first.alg << ":" << p.first.len << ":" << base64_encode(p.second->key()) << std::endl;
}
ss << info.alg << ":" << info.len << ":" << base64_encode(k->key()) << std::endl;
auto s = ss.str();
auto tmpnam = _path + ".tmp";
auto f = make_ready_future<>();
if (!_must_exist) {
f = seastar::recursive_touch_directory((bfs::path(tmpnam).remove_filename()).string());
}
return f.then([this, tmpnam, s] {
return write_text_file_fully(tmpnam, s).then([this, tmpnam] {
return rename_file(tmpnam, _path);
});
}).then([this, k, info] {
// don't cache until written
_keys[info] = k;
return make_ready_future<key_ptr>(k);
});
}).handle_exception([this](auto ep) -> key_ptr{
try {
std::rethrow_exception(ep);
} catch (...) {
std::throw_with_nested(service_error("Could not write key file '" + _path + "'"));
}
});
}
local_system_key::local_system_key(encryption_context& ctxt, const sstring& path)
: _provider(make_shared<local_file_provider>(ctxt, bfs::path(ctxt.config().system_key_directory()) / bfs::path(path), true))
{}
local_system_key::~local_system_key()
{}
future<shared_ptr<symmetric_key>> local_system_key::get_key() {
return _provider->key(system_key_info).then([](std::tuple<key_ptr, opt_bytes> k_id) {
return make_ready_future<shared_ptr<symmetric_key>>(std::get<0>(std::move(k_id)));
});
}
future<> local_system_key::validate() const {
// first, just validate the file provider itself
co_await _provider->validate();
// second, do an early load of the actual key to ensure file contents.
co_await _provider->key(system_key_info);
}
const sstring& local_system_key::name() const {
return _provider->path();
}
}