Signet Forge 0.1.1
C++20 Parquet library with AI-native extensions
DEMO
Loading...
Searching...
No Matches
kms_local.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright 2026 Johnson Ogundeji
3// SPDX-License-Identifier: AGPL-3.0-or-later
4// See LICENSE_COMMERCIAL for full terms.
5#pragma once
6
7#if !defined(SIGNET_ENABLE_COMMERCIAL) || !SIGNET_ENABLE_COMMERCIAL
8#error "signet/crypto/kms_local.hpp is a AGPL-3.0 + Commercial Exception commercial module. Build with -DSIGNET_ENABLE_COMMERCIAL=ON."
9#endif
10
32
34#include "signet/crypto/hsm_client_stub.hpp" // IKmsClient, AES Key Wrap
36#include "signet/error.hpp"
37
38#include <array>
39#include <chrono>
40#include <cerrno>
41#include <cstdint>
42#include <cstdio>
43#include <cstring>
44#include <fstream>
45#include <mutex>
46#include <string>
47#include <sys/stat.h>
48#include <unordered_map>
49#include <vector>
50
51namespace signet::forge::crypto {
52
72class LocalKeyStore : public IKmsClient {
73public:
74 struct Config {
75 std::string keystore_path;
76 std::string passphrase;
77 bool create_if_missing = true;
81 uint32_t pbkdf2_iterations = 600'000u;
82 };
83
85 explicit LocalKeyStore(Config config)
86 : config_(std::move(config))
87 {
88 derive_kek();
89 }
90
91 ~LocalKeyStore() override {
92 // Secure-zero KEK on destruction
93 secure_zero(kek_.data(), kek_.size());
94 // Secure-zero cached keys
95 for (auto& [id, key] : keys_)
96 secure_zero(key.data(), key.size());
97 }
98
99 LocalKeyStore(const LocalKeyStore&) = delete;
101
102 // --- IKmsClient interface (const, thread-safe via mutable mutex) ---
103
106 const std::vector<uint8_t>& dek,
107 const std::string& key_id) const override
108 {
109 std::lock_guard<std::mutex> lock(mu_);
110 auto master = load_key_internal(key_id);
111 if (!master) return master.error();
112
113 std::array<uint8_t, 32> master_arr{};
114 std::memcpy(master_arr.data(), master->data(),
115 std::min(master->size(), size_t(32)));
116
117 auto result = detail::aes_key_wrap::wrap(master_arr, dek);
118 secure_zero(master_arr.data(), master_arr.size());
119 log_access(key_id, "wrap");
120 return result;
121 }
122
125 const std::vector<uint8_t>& wrapped_dek,
126 const std::string& key_id) const override
127 {
128 std::lock_guard<std::mutex> lock(mu_);
129 auto master = load_key_internal(key_id);
130 if (!master) return master.error();
131
132 std::array<uint8_t, 32> master_arr{};
133 std::memcpy(master_arr.data(), master->data(),
134 std::min(master->size(), size_t(32)));
135
136 auto result = detail::aes_key_wrap::unwrap(master_arr, wrapped_dek);
137 secure_zero(master_arr.data(), master_arr.size());
138 log_access(key_id, "unwrap");
139 return result;
140 }
141
142 // --- Extended key lifecycle methods (not part of IKmsClient) ---
143
145 [[nodiscard]] expected<std::string> generate_key(const std::string& key_id) {
146 std::lock_guard<std::mutex> lock(mu_);
147 std::array<uint8_t, 32> key{};
148 csprng_fill(key.data(), key.size());
149 std::vector<uint8_t> key_vec(key.begin(), key.end());
150
151 auto result = store_key(key_id, key_vec);
152 secure_zero(key.data(), key.size());
153 if (!result) return result.error();
154
155 log_access(key_id, "generate");
156 return key_id;
157 }
158
160 [[nodiscard]] expected<void> destroy_key(const std::string& key_id) {
161 std::lock_guard<std::mutex> lock(mu_);
162 auto it = keys_.find(key_id);
163 if (it != keys_.end()) {
164 secure_zero(it->second.data(), it->second.size());
165 keys_.erase(it);
166 }
167
168 std::string path = key_file_path(key_id);
169 std::remove(path.c_str());
170
171 log_access(key_id, "destroy");
172 return expected<void>{};
173 }
174
176 [[nodiscard]] bool has_key(const std::string& key_id) const {
177 std::lock_guard<std::mutex> lock(mu_);
178 if (keys_.find(key_id) != keys_.end()) return true;
179 std::ifstream ifs(key_file_path(key_id), std::ios::binary);
180 return ifs.good();
181 }
182
183private:
184 Config config_;
185 std::array<uint8_t, 32> kek_{};
186 mutable std::unordered_map<std::string, std::vector<uint8_t>> keys_;
187 mutable std::mutex mu_;
188
194 static std::array<uint8_t, 32> pbkdf2_sha256_32(
195 const uint8_t* password, size_t password_len,
196 const uint8_t* salt, size_t salt_len,
197 uint32_t iterations) {
198 // U_1 = HMAC-SHA256(password, salt || INT(1))
199 std::vector<uint8_t> u1_input;
200 u1_input.reserve(salt_len + 4u);
201 u1_input.insert(u1_input.end(), salt, salt + salt_len);
202 // Block index 1 as 4-byte big-endian
203 u1_input.push_back(0x00u);
204 u1_input.push_back(0x00u);
205 u1_input.push_back(0x00u);
206 u1_input.push_back(0x01u);
207
209 password, password_len, u1_input.data(), u1_input.size());
210
211 // Zeroize the salt+INT(1) buffer (CWE-316)
212 volatile uint8_t* vp = u1_input.data();
213 for (size_t i = 0; i < u1_input.size(); ++i) vp[i] = 0u;
214
215 // DK = U_1 ^ U_2 ^ ... ^ U_c
216 std::array<uint8_t, 32> dk = u;
217 for (uint32_t j = 1u; j < iterations; ++j) {
218 u = detail::hkdf::hmac_sha256(password, password_len, u.data(), u.size());
219 for (size_t k = 0; k < 32u; ++k) dk[k] ^= u[k];
220 }
221
222 // Zeroize last U block (CWE-316)
223 volatile uint8_t* vpu = u.data();
224 for (size_t i = 0; i < 32u; ++i) vpu[i] = 0u;
225
226 return dk;
227 }
228
229 void derive_kek() {
230 static constexpr uint8_t kek_salt[] = "signet:local-keystore:kek:v1";
231 static constexpr uint8_t kek_info[] = "signet:kek-derivation";
232
233 auto passphrase_bytes = reinterpret_cast<const uint8_t*>(config_.passphrase.data());
234
235 // F4: PBKDF2-SHA256 password-stretching before HKDF provides work factor
236 // for low-entropy operator passphrases (NIST SP 800-132, OWASP 2023).
237 // The stretched output replaces raw passphrase bytes as the IKM for HKDF.
238 auto stretched = pbkdf2_sha256_32(
239 passphrase_bytes, config_.passphrase.size(),
240 kek_salt, sizeof(kek_salt) - 1u,
241 config_.pbkdf2_iterations);
242
243 auto prk = hkdf_extract(kek_salt, sizeof(kek_salt) - 1,
244 stretched.data(), stretched.size());
245
246 // Zeroize stretched key material before stack is reused (CWE-316)
247 volatile uint8_t* vp = stretched.data();
248 for (size_t i = 0; i < stretched.size(); ++i) vp[i] = 0u;
249
250 (void)hkdf_expand(prk, kek_info, sizeof(kek_info) - 1, kek_.data(), kek_.size());
251 }
252
253 static void secure_zero(void* ptr, size_t len) {
254 volatile auto* p = static_cast<volatile uint8_t*>(ptr);
255 while (len--) *p++ = 0;
256 }
257
258 static void csprng_fill(uint8_t* buf, size_t len) {
259#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
260 arc4random_buf(buf, len);
261#elif defined(__linux__)
262 // F3: Only EINTR is retryable. All other negative returns (ENOSYS,
263 // EFAULT, EINVAL) indicate a permanently broken entropy source.
264 // Rather than loop indefinitely, terminate — continuing with
265 // uninitialized key material is catastrophically worse than crashing
266 // (consistent with OpenSSL / libsodium abort-on-CSPRNG-failure policy).
267 static constexpr int kMaxRetries = 100;
268 int retries = 0;
269 while (len > 0) {
270 auto got = getrandom(buf, len, 0);
271 if (got < 0) {
272 if (errno == EINTR && ++retries < kMaxRetries) continue;
273 // Non-retryable error or retry limit exceeded — hard fail.
274 std::terminate();
275 }
276 retries = 0;
277 buf += got;
278 len -= static_cast<size_t>(got);
279 }
280#elif defined(_WIN32)
281 BCryptGenRandom(nullptr, buf, static_cast<ULONG>(len), BCRYPT_USE_SYSTEM_PREFERRED_RNG);
282#endif
283 }
284
285 std::string key_file_path(const std::string& key_id) const {
286 return config_.keystore_path + "/keys/" + key_id + ".key";
287 }
288
290 [[nodiscard]] expected<std::vector<uint8_t>> load_key_internal(const std::string& key_id) const {
291 auto it = keys_.find(key_id);
292 if (it != keys_.end()) return it->second;
293
294 std::string path = key_file_path(key_id);
295 std::ifstream ifs(path, std::ios::binary);
296 if (!ifs) {
297 return Error{ErrorCode::ENCRYPTION_ERROR, "key not found: " + key_id};
298 }
299
300 std::vector<uint8_t> wrapped((std::istreambuf_iterator<char>(ifs)),
301 std::istreambuf_iterator<char>());
302 ifs.close();
303
304 auto plaintext = detail::aes_key_wrap::unwrap(kek_, wrapped);
305 if (!plaintext) return plaintext.error();
306
307 keys_[key_id] = *plaintext;
308 return *plaintext;
309 }
310
312 [[nodiscard]] expected<void> store_key(const std::string& key_id,
313 const std::vector<uint8_t>& plaintext) {
314 auto wrapped = detail::aes_key_wrap::wrap(kek_, plaintext);
315 if (!wrapped) return wrapped.error();
316
317 std::string dir = config_.keystore_path + "/keys";
318#if defined(_WIN32)
319 (void)_mkdir(config_.keystore_path.c_str());
320 (void)_mkdir(dir.c_str());
321#else
322 (void)::mkdir(config_.keystore_path.c_str(), 0700);
323 (void)::mkdir(dir.c_str(), 0700);
324#endif
325
326 std::string path = key_file_path(key_id);
327 std::ofstream ofs(path, std::ios::binary | std::ios::trunc);
328 if (!ofs) {
329 return Error{ErrorCode::IO_ERROR, "failed to write key file: " + path};
330 }
331 ofs.write(reinterpret_cast<const char*>(wrapped->data()),
332 static_cast<std::streamsize>(wrapped->size()));
333 ofs.close();
334
335#if !defined(_WIN32)
336 ::chmod(path.c_str(), 0600);
337#endif
338
339 keys_[key_id] = plaintext;
340 return expected<void>{};
341 }
342
344 void log_access(const std::string& key_id, const char* operation) const {
345 std::string log_path = config_.keystore_path + "/audit.log";
346 std::ofstream log_file(log_path, std::ios::app);
347 if (!log_file) return;
348
349 auto now = std::chrono::system_clock::now();
350 auto epoch = std::chrono::duration_cast<std::chrono::seconds>(
351 now.time_since_epoch()).count();
352
353 log_file << epoch << " " << operation << " " << key_id << "\n";
354 }
355};
356
357} // namespace signet::forge::crypto
Abstract KMS client interface for DEK/KEK key wrapping.
File-based local key store for on-premise deployments.
Definition kms_local.hpp:72
LocalKeyStore(const LocalKeyStore &)=delete
expected< std::vector< uint8_t > > unwrap_key(const std::vector< uint8_t > &wrapped_dek, const std::string &key_id) const override
Unwrap (decrypt) a wrapped DEK using the master key identified by key_id.
LocalKeyStore & operator=(const LocalKeyStore &)=delete
expected< void > destroy_key(const std::string &key_id)
Destroy a master key (crypto-shredding for GDPR Art. 17).
expected< std::string > generate_key(const std::string &key_id)
Generate a new AES-256 master key and store it under key_id.
LocalKeyStore(Config config)
Construct a LocalKeyStore from configuration.
Definition kms_local.hpp:85
expected< std::vector< uint8_t > > wrap_key(const std::vector< uint8_t > &dek, const std::string &key_id) const override
Wrap (encrypt) a DEK under the master key identified by key_id.
bool has_key(const std::string &key_id) const
Check if a key exists in the store (cached or on disk).
A lightweight result type that holds either a success value of type T or an Error.
Definition error.hpp:143
HKDF key derivation (RFC 5869) using HMAC-SHA256.
Key material, encryption configuration, and TLV serialization for Parquet Modular Encryption (PME).
expected< std::vector< uint8_t > > wrap(const std::array< uint8_t, 32 > &kek, const std::vector< uint8_t > &plaintext)
AES Key Wrap — wraps plaintext key material under a KEK.
expected< std::vector< uint8_t > > unwrap(const std::array< uint8_t, 32 > &kek, const std::vector< uint8_t > &ciphertext)
AES Key Unwrap — recovers plaintext key material from wrapped form.
std::array< uint8_t, 32 > hmac_sha256(const uint8_t *key, size_t key_size, const uint8_t *data, size_t data_size)
HMAC-SHA256 (RFC 2104): keyed hash for HKDF.
Definition hkdf.hpp:44
std::array< uint8_t, 32 > hkdf_extract(const uint8_t *salt, size_t salt_size, const uint8_t *ikm, size_t ikm_size)
HKDF-Extract (RFC 5869 §2.2): Extract a pseudorandom key from input keying material.
Definition hkdf.hpp:107
bool hkdf_expand(const std::array< uint8_t, 32 > &prk, const uint8_t *info, size_t info_size, uint8_t *output, size_t output_size)
HKDF-Expand (RFC 5869 §2.3): Expand PRK to output keying material.
Definition hkdf.hpp:126
@ IO_ERROR
A file-system or stream I/O operation failed (open, read, write, rename).
@ ENCRYPTION_ERROR
An encryption or decryption operation failed (bad key, tampered ciphertext, PME error).
std::string keystore_path
Directory path (e.g. ~/.signet/keystore)
Definition kms_local.hpp:75
std::string passphrase
Passphrase for KEK derivation.
Definition kms_local.hpp:76
uint32_t pbkdf2_iterations
PBKDF2-SHA256 iteration count for passphrase → KEK stretching.
Definition kms_local.hpp:81
bool create_if_missing
Create keystore directory on first use.
Definition kms_local.hpp:77