Signet Forge 0.1.0
C++20 Parquet library with AI-native extensions
DEMO
Loading...
Searching...
No Matches
cipher_interface.hpp
Go to the documentation of this file.
1// SPDX-License-Identifier: AGPL-3.0-or-later
2// Copyright 2026 Johnson Ogundeji
3#pragma once
4
7
8// ---------------------------------------------------------------------------
9// cipher_interface.hpp -- Abstract cipher interface + CipherFactory
10//
11// Provides crypto-agility for Parquet Modular Encryption by abstracting
12// the cipher behind an ICipher interface. Concrete adapters wrap AesGcm
13// and AesCtr. CipherFactory selects the correct cipher for each PME role
14// (footer, column data, metadata) based on the EncryptionAlgorithm enum.
15//
16// Wire format (unified across all ciphers):
17// [1 byte: iv_size] [iv bytes] [ciphertext (+tag for GCM)]
18//
19// The interface is AGPL-3.0-or-later (core tier) — always available with SIGNET_ENABLE_COMMERCIAL.
20// ---------------------------------------------------------------------------
21
26#include "signet/error.hpp"
27
28#include <array>
29#include <atomic>
30#include <cstddef>
31#include <cstdint>
32#include <cstring>
33#include <functional>
34#include <limits>
35#include <memory>
36#include <string>
37#include <string_view>
38#include <vector>
39
40// Platform-specific CSPRNG + mlock headers
41#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
42# include <stdlib.h> // arc4random_buf
43# include <sys/mman.h> // mlock, munlock (Gap C-11)
44#elif defined(__linux__)
45# include <sys/random.h> // getrandom
46# include <sys/mman.h> // mlock, munlock (Gap C-11)
47#elif defined(_WIN32)
48# include <windows.h> // VirtualLock, VirtualUnlock, SecureZeroMemory
49# include <bcrypt.h>
50#endif
51
52#include <cerrno>
53#include <stdexcept>
54
55namespace signet::forge::crypto {
56
57// ===========================================================================
58// ICipher -- Abstract cipher interface
59// ===========================================================================
60
64class ICipher {
65public:
66 virtual ~ICipher() = default;
67
71 [[nodiscard]] virtual expected<std::vector<uint8_t>> encrypt(
72 const uint8_t* data, size_t size,
73 const std::string& aad = "") const = 0;
74
76 [[nodiscard]] virtual expected<std::vector<uint8_t>> decrypt(
77 const uint8_t* data, size_t size,
78 const std::string& aad = "") const = 0;
79
81 [[nodiscard]] virtual bool is_authenticated() const noexcept = 0;
82
84 [[nodiscard]] virtual size_t key_size() const noexcept = 0;
85
87 [[nodiscard]] virtual std::string_view algorithm_name() const noexcept = 0;
88
89 // Non-copyable, non-movable (interface type)
90 ICipher() = default;
91 ICipher(const ICipher&) = delete;
92 ICipher& operator=(const ICipher&) = delete;
93 ICipher(ICipher&&) = default;
94 ICipher& operator=(ICipher&&) = default;
95};
96
97// ===========================================================================
98// Internal: IV generation (shared by both adapters)
99// ===========================================================================
100
101namespace detail::cipher {
102
108inline void fill_random_bytes(uint8_t* buf, size_t size) {
109 if (size == 0) return;
110#if defined(__APPLE__) || defined(__FreeBSD__) || defined(__OpenBSD__)
111 arc4random_buf(buf, size);
112#elif defined(__linux__)
113 // getrandom() with flags=0 blocks until urandom is seeded
114 size_t written = 0;
115 while (written < size) {
116 ssize_t ret = getrandom(buf + written, size - written, 0);
117 if (ret < 0) {
118 if (errno == EINTR) continue; // retry on signal interrupt
119 // Real error — zero partial output to avoid leaking partial randomness
120 volatile unsigned char* p = buf;
121 for (size_t i = 0; i < size; ++i) p[i] = 0;
122 throw std::runtime_error("signet: getrandom() failed");
123 }
124 written += static_cast<size_t>(ret);
125 }
126#elif defined(_WIN32)
127 if (size > static_cast<size_t>((std::numeric_limits<ULONG>::max)())) {
128 throw std::runtime_error("csprng_fill: size exceeds ULONG max");
129 }
130 NTSTATUS status = BCryptGenRandom(NULL, buf, static_cast<ULONG>(size),
131 BCRYPT_USE_SYSTEM_PREFERRED_RNG);
132 if (status != 0) {
133 throw std::runtime_error("BCryptGenRandom failed");
134 }
135#else
136 throw std::runtime_error("signet: no secure RNG available on this platform");
137#endif
138}
139
143inline std::vector<uint8_t> generate_iv(size_t iv_size) {
144 std::vector<uint8_t> iv(iv_size);
145 fill_random_bytes(iv.data(), iv_size);
146 return iv;
147}
148
153inline std::vector<uint8_t> prepend_iv(const std::vector<uint8_t>& iv,
154 const std::vector<uint8_t>& ciphertext) {
155 std::vector<uint8_t> out;
156 out.reserve(1 + iv.size() + ciphertext.size());
157 out.push_back(static_cast<uint8_t>(iv.size()));
158 out.insert(out.end(), iv.begin(), iv.end());
159 out.insert(out.end(), ciphertext.begin(), ciphertext.end());
160 return out;
161}
162
164struct IvParsed {
165 const uint8_t* iv;
166 const uint8_t* ciphertext;
167 size_t ct_size;
168};
169
174inline expected<IvParsed> parse_iv_header(const uint8_t* data, size_t size) {
175 if (size < 1) {
177 "cipher: encrypted data too short (no IV size byte)"};
178 }
179 uint8_t iv_size = data[0];
180 if (iv_size == 0 || iv_size > 16) {
182 "cipher: invalid IV size " + std::to_string(iv_size)};
183 }
184 size_t header_len = 1 + static_cast<size_t>(iv_size);
185 if (size < header_len) {
187 "cipher: encrypted data too short for IV"};
188 }
189 return IvParsed{data + 1, data + header_len, size - header_len};
190}
191
192} // namespace detail::cipher
193
194// ===========================================================================
195// Continuous Random Number Generator Test (CRNGT) wrapper (Gap C-13)
196//
197// FIPS 140-3 §4.9.2 requires a continuous test on the RNG output:
198// each block of random output must differ from the previous block.
199// If two consecutive blocks are identical, the RNG has failed and
200// the module must enter an error state.
201//
202// This wrapper generates random bytes via the platform CSPRNG and
203// compares each 32-byte block against the previous output. On
204// failure, it throws (entering an error state per FIPS 140-3).
205//
206// Reference: FIPS 140-3 §4.9.2 — Continuous random number generator test
207// NIST SP 800-90B §4 — Health tests for entropy sources
208// ===========================================================================
209
210namespace detail::crngt {
211
214 uint8_t prev[32] = {};
215 bool initialized = false;
216};
217
228 uint8_t* buf, size_t size) {
229 detail::cipher::fill_random_bytes(buf, size);
230
231 // Test in 32-byte blocks
232 size_t offset = 0;
233 while (offset + 32 <= size) {
234 if (state.initialized) {
235 if (std::memcmp(buf + offset, state.prev, 32) == 0) {
236 throw std::runtime_error(
237 "CRNGT failure: consecutive RNG outputs are identical "
238 "(FIPS 140-3 §4.9.2)");
239 }
240 }
241 std::memcpy(state.prev, buf + offset, 32);
242 state.initialized = true;
243 offset += 32;
244 }
245
246 // Handle trailing bytes < 32 — FIPS 140-3 §4.9.2 requires failure on
247 // consecutive identical blocks, including partial trailing blocks.
248 if (offset < size && state.initialized) {
249 size_t remaining = size - offset;
250 if (remaining > 0 && std::memcmp(buf + offset, state.prev, remaining) == 0) {
251 throw std::runtime_error("CRNGT: consecutive identical output blocks detected (partial)");
252 }
253 }
254}
255
256} // namespace detail::crngt
257
258// ===========================================================================
259// Secure memory utilities (Gap C-11)
260//
261// Prevents key material from being paged to swap (mlock) and ensures
262// zeroization on deallocation. On platforms without mlock (Windows),
263// VirtualLock is used instead.
264//
265// Reference: NIST SP 800-57 Part 1 Rev. 5 §8.2.2 (key protection)
266// FIPS 140-3 §4.7.6 (key material zeroization)
267// ===========================================================================
268
269namespace detail::secure_mem {
270
273inline bool lock_memory(void* ptr, size_t size) {
274 if (!ptr || size == 0) return false;
275#if defined(_WIN32)
276 return VirtualLock(ptr, size) != 0;
277#elif defined(__EMSCRIPTEN__)
278 (void)ptr; (void)size;
279 return false; // mlock not available in WASM
280#elif defined(__unix__) || defined(__APPLE__)
281 return ::mlock(ptr, size) == 0;
282#else
283 (void)ptr; (void)size;
284 return false;
285#endif
286}
287
289inline void unlock_memory(void* ptr, size_t size) {
290 if (!ptr || size == 0) return;
291#if defined(_WIN32)
292 VirtualUnlock(ptr, size);
293#elif defined(__EMSCRIPTEN__)
294 (void)ptr; (void)size; // munlock not available in WASM
295#elif defined(__unix__) || defined(__APPLE__)
296 ::munlock(ptr, size);
297#else
298 (void)ptr; (void)size;
299#endif
300}
301
303inline void secure_zero(void* ptr, size_t size) {
304 if (!ptr || size == 0) return;
305#if defined(_WIN32)
306 SecureZeroMemory(ptr, size);
307#else
308 volatile unsigned char* p = static_cast<volatile unsigned char*>(ptr);
309 for (size_t i = 0; i < size; ++i) p[i] = 0;
310#endif
311}
312
313} // namespace detail::secure_mem
314
321public:
323 explicit SecureKeyBuffer(const std::vector<uint8_t>& key)
324 : data_(key) {
325 detail::secure_mem::lock_memory(data_.data(), data_.size());
326 }
327
329 SecureKeyBuffer(const uint8_t* ptr, size_t size)
330 : data_(ptr, ptr + size) {
331 detail::secure_mem::lock_memory(data_.data(), data_.size());
332 }
333
335 explicit SecureKeyBuffer(size_t size) : data_(size) {
336 detail::cipher::fill_random_bytes(data_.data(), size);
337 detail::secure_mem::lock_memory(data_.data(), data_.size());
338 }
339
341 detail::secure_mem::secure_zero(data_.data(), data_.size());
342 detail::secure_mem::unlock_memory(data_.data(), data_.size());
343 }
344
345 // Move-only
346 SecureKeyBuffer(SecureKeyBuffer&& other) noexcept : data_(std::move(other.data_)) {
347 if (!data_.empty())
348 detail::secure_mem::lock_memory(data_.data(), data_.size());
349 }
351 if (this != &other) {
352 detail::secure_mem::secure_zero(data_.data(), data_.size());
353 detail::secure_mem::unlock_memory(data_.data(), data_.size());
354 data_ = std::move(other.data_);
355 }
356 return *this;
357 }
360
361 [[nodiscard]] const uint8_t* data() const { return data_.data(); }
362 [[nodiscard]] uint8_t* data() { return data_.data(); }
363 [[nodiscard]] size_t size() const { return data_.size(); }
364 [[nodiscard]] bool empty() const { return data_.empty(); }
365
366private:
367 std::vector<uint8_t> data_;
368};
369
370// ===========================================================================
371// AesGcmCipher -- AES-256-GCM adapter
372// ===========================================================================
373
387class AesGcmCipher final : public ICipher {
388public:
392 static constexpr uint64_t MAX_INVOCATIONS = UINT64_C(0xFFFFFFFF); // 2^32 - 1
393
395 static constexpr uint64_t DEFAULT_ROTATION_THRESHOLD =
396 static_cast<uint64_t>(MAX_INVOCATIONS * 0.75);
397
401 using RotationCallback = std::function<void(uint64_t invocation_count)>;
402
404 explicit AesGcmCipher(const std::vector<uint8_t>& key)
405 : key_{}, gcm_(nullptr) {
406 std::memcpy(key_.data(), key.data(), (std::min)(key.size(), key_.size()));
407 gcm_ = std::make_unique<AesGcm>(key_.data());
408 }
409
411 explicit AesGcmCipher(const uint8_t* key, size_t key_len)
412 : key_{}, gcm_(nullptr) {
413 std::memcpy(key_.data(), key, (std::min)(key_len, key_.size()));
414 gcm_ = std::make_unique<AesGcm>(key_.data());
415 }
416
423 uint64_t threshold = DEFAULT_ROTATION_THRESHOLD) {
424 rotation_callback_ = std::move(cb);
425 rotation_threshold_ = threshold;
426 }
427
429 [[nodiscard]] uint64_t invocation_count() const noexcept {
430 return invocation_count_.load(std::memory_order_relaxed);
431 }
432
434 const uint8_t* data, size_t size,
435 const std::string& aad = "") const override {
436
437 if (key_.size() != AesGcm::KEY_SIZE) {
438 return Error{ErrorCode::ENCRYPTION_ERROR,
439 "AesGcmCipher: key must be 32 bytes"};
440 }
441
442 // NIST SP 800-38D §8.2: Enforce invocation limit for random-IV GCM.
443 // With 96-bit random IVs, birthday collision probability exceeds
444 // acceptable bounds after 2^32 invocations under the same key.
445 uint64_t count = invocation_count_.fetch_add(1, std::memory_order_relaxed);
446 if (count >= MAX_INVOCATIONS) {
447 return Error{ErrorCode::ENCRYPTION_ERROR,
448 "AES-GCM: key invocation limit reached (2^32). "
449 "NIST SP 800-38D §8.2 requires key rotation."};
450 }
451
452 // Trigger rotation callback at threshold (fire once)
453 if (rotation_callback_ && count == rotation_threshold_) {
454 rotation_callback_(count);
455 }
456
457 auto iv = detail::cipher::generate_iv(AesGcm::IV_SIZE);
458
459 auto result = aad.empty()
460 ? gcm_->encrypt(data, size, iv.data())
461 : gcm_->encrypt(data, size, iv.data(),
462 reinterpret_cast<const uint8_t*>(aad.data()),
463 aad.size());
464
465 if (!result) return result.error();
466 return detail::cipher::prepend_iv(iv, *result);
467 }
468
470 const uint8_t* data, size_t size,
471 const std::string& aad = "") const override {
472
473 if (key_.size() != AesGcm::KEY_SIZE) {
474 return Error{ErrorCode::ENCRYPTION_ERROR,
475 "AesGcmCipher: key must be 32 bytes"};
476 }
477
478 auto iv_result = detail::cipher::parse_iv_header(data, size);
479 if (!iv_result) return iv_result.error();
480 const auto& [iv, ciphertext, ct_size] = *iv_result;
481
482 if (!aad.empty()) {
483 return gcm_->decrypt(ciphertext, ct_size, iv,
484 reinterpret_cast<const uint8_t*>(aad.data()),
485 aad.size());
486 } else {
487 return gcm_->decrypt(ciphertext, ct_size, iv);
488 }
489 }
490
493 ~AesGcmCipher() override {
494 volatile uint8_t* p = key_.data();
495 for (size_t i = 0; i < key_.size(); ++i) p[i] = 0;
496#if defined(__GNUC__) || defined(__clang__)
497 __asm__ __volatile__("" ::: "memory");
498#endif
499 }
500
501 [[nodiscard]] bool is_authenticated() const noexcept override { return true; }
502 [[nodiscard]] size_t key_size() const noexcept override { return AesGcm::KEY_SIZE; }
503 [[nodiscard]] std::string_view algorithm_name() const noexcept override {
504 return "AES-256-GCM";
505 }
506
507private:
508 std::array<uint8_t, 32> key_{};
509 std::unique_ptr<AesGcm> gcm_;
510 mutable std::atomic<uint64_t> invocation_count_{0};
511 RotationCallback rotation_callback_;
512 uint64_t rotation_threshold_{DEFAULT_ROTATION_THRESHOLD};
513};
514
515// ===========================================================================
516// AesCtrCipher -- AES-256-CTR adapter
517// ===========================================================================
518
526class AesCtrCipher final : public ICipher {
527public:
528 static constexpr uint8_t AAD_TAGGED_IV_FLAG = 0x80u;
529 static constexpr size_t AAD_TAG_SIZE = 32u;
530
532 explicit AesCtrCipher(const std::vector<uint8_t>& key)
533 : key_{}, ctr_(nullptr) {
534 std::memcpy(key_.data(), key.data(), (std::min)(key.size(), key_.size()));
535 ctr_ = std::make_unique<AesCtr>(key_.data());
536 }
537
539 explicit AesCtrCipher(const uint8_t* key, size_t key_len)
540 : key_{}, ctr_(nullptr) {
541 std::memcpy(key_.data(), key, (std::min)(key_len, key_.size()));
542 ctr_ = std::make_unique<AesCtr>(key_.data());
543 }
544
546 const uint8_t* data, size_t size,
547 const std::string& aad = "") const override {
548
549 if (key_.size() != AesCtr::KEY_SIZE) {
550 return Error{ErrorCode::ENCRYPTION_ERROR,
551 "AesCtrCipher: key must be 32 bytes"};
552 }
553
554 auto iv = detail::cipher::generate_iv(AesCtr::IV_SIZE);
555 auto ct_result = ctr_->encrypt(data, size, iv.data());
556 if (!ct_result) return ct_result.error();
557
558 if (aad.empty()) {
559 return detail::cipher::prepend_iv(iv, *ct_result);
560 }
561
562 auto mac_key = derive_aad_mac_key();
563 auto tag = compute_aad_tag(mac_key, aad, iv.data(), iv.size(),
564 ct_result->data(), ct_result->size());
565
566 std::vector<uint8_t> out;
567 out.reserve(1 + iv.size() + ct_result->size() + tag.size());
568 out.push_back(static_cast<uint8_t>(iv.size()) | AAD_TAGGED_IV_FLAG);
569 out.insert(out.end(), iv.begin(), iv.end());
570 out.insert(out.end(), ct_result->begin(), ct_result->end());
571 out.insert(out.end(), tag.begin(), tag.end());
572 return out;
573 }
574
576 const uint8_t* data, size_t size,
577 const std::string& aad = "") const override {
578
579 if (key_.size() != AesCtr::KEY_SIZE) {
580 return Error{ErrorCode::ENCRYPTION_ERROR,
581 "AesCtrCipher: key must be 32 bytes"};
582 }
583 if (size < 1) {
584 return Error{ErrorCode::ENCRYPTION_ERROR,
585 "AesCtrCipher: encrypted data too short"};
586 }
587
588 const uint8_t iv_flag = data[0];
589 const bool tagged = (iv_flag & AAD_TAGGED_IV_FLAG) != 0;
590 if (tagged) {
591 const size_t iv_size = static_cast<size_t>(iv_flag & ~AAD_TAGGED_IV_FLAG);
592 const size_t header_size = 1 + iv_size;
593 if (iv_size == 0 || iv_size > AesCtr::IV_SIZE || size < header_size + AAD_TAG_SIZE) {
594 return Error{ErrorCode::ENCRYPTION_ERROR,
595 "AesCtrCipher: invalid tagged CTR payload"};
596 }
597 if (aad.empty()) {
598 return Error{ErrorCode::ENCRYPTION_ERROR,
599 "AesCtrCipher: tagged CTR payload requires AAD"};
600 }
601
602 const uint8_t* iv = data + 1;
603 const size_t ct_size = size - header_size - AAD_TAG_SIZE;
604 const uint8_t* ciphertext = data + header_size;
605 const uint8_t* stored_tag = ciphertext + ct_size;
606
607 auto mac_key = derive_aad_mac_key();
608 auto expected_tag = compute_aad_tag(mac_key, aad, iv, iv_size, ciphertext, ct_size);
609 uint8_t diff = 0;
610 for (size_t i = 0; i < expected_tag.size(); ++i) {
611 diff |= static_cast<uint8_t>(stored_tag[i] ^ expected_tag[i]);
612 }
613 if (diff != 0) {
614 return Error{ErrorCode::ENCRYPTION_ERROR,
615 "AesCtrCipher: AAD authentication failed"};
616 }
617
618 auto pt_result = ctr_->decrypt(ciphertext, ct_size, iv);
619 if (!pt_result) return pt_result.error();
620 return std::move(*pt_result);
621 }
622
623 auto iv_result = detail::cipher::parse_iv_header(data, size);
624 if (!iv_result) return iv_result.error();
625 const auto& [iv, ciphertext, ct_size] = *iv_result;
626
627 auto pt_result = ctr_->decrypt(ciphertext, ct_size, iv);
628 if (!pt_result) return pt_result.error();
629 return std::move(*pt_result);
630 }
631
634 ~AesCtrCipher() override {
635 volatile uint8_t* p = key_.data();
636 for (size_t i = 0; i < key_.size(); ++i) p[i] = 0;
637#if defined(__GNUC__) || defined(__clang__)
638 __asm__ __volatile__("" ::: "memory");
639#endif
640 }
641
643 [[nodiscard]] bool is_authenticated() const noexcept override { return false; }
645 [[nodiscard]] size_t key_size() const noexcept override { return AesCtr::KEY_SIZE; }
647 [[nodiscard]] std::string_view algorithm_name() const noexcept override {
648 return "AES-256-CTR";
649 }
650
651private:
652 [[nodiscard]] std::array<uint8_t, 32> derive_aad_mac_key() const {
653 static constexpr uint8_t kInfo[] = "signet-aes-ctr-aad-mac-v1";
654 auto prk = hkdf_extract(nullptr, 0, key_.data(), key_.size());
655 std::array<uint8_t, 32> mac_key{};
656 (void)hkdf_expand(prk, kInfo, sizeof(kInfo) - 1, mac_key.data(), mac_key.size());
657 return mac_key;
658 }
659
660 [[nodiscard]] static std::array<uint8_t, AAD_TAG_SIZE> compute_aad_tag(
661 const std::array<uint8_t, 32>& mac_key,
662 const std::string& aad,
663 const uint8_t* iv, size_t iv_size,
664 const uint8_t* ciphertext, size_t ct_size) {
665
666 std::vector<uint8_t> mac_input;
667 mac_input.reserve(aad.size() + iv_size + ct_size);
668 mac_input.insert(mac_input.end(), aad.begin(), aad.end());
669 mac_input.insert(mac_input.end(), iv, iv + iv_size);
670 mac_input.insert(mac_input.end(), ciphertext, ciphertext + ct_size);
671 return detail::hkdf::hmac_sha256(mac_key.data(), mac_key.size(),
672 mac_input.data(), mac_input.size());
673 }
674
675 std::array<uint8_t, 32> key_{};
676 std::unique_ptr<AesCtr> ctr_;
677};
678
679// ===========================================================================
680// CipherFactory -- static factory for creating cipher instances
681// ===========================================================================
682
687 [[nodiscard]] static std::unique_ptr<ICipher> create_footer_cipher(
688 EncryptionAlgorithm /*algo*/, const std::vector<uint8_t>& key) {
689 // Footer always uses GCM regardless of algorithm
690 return std::make_unique<AesGcmCipher>(key);
691 }
692
694 [[nodiscard]] static std::unique_ptr<ICipher> create_column_cipher(
695 EncryptionAlgorithm algo, const std::vector<uint8_t>& key) {
696 if (algo == EncryptionAlgorithm::AES_GCM_V1) {
697 return std::make_unique<AesGcmCipher>(key);
698 }
699 // AES_GCM_CTR_V1: column data uses CTR
700 return std::make_unique<AesCtrCipher>(key);
701 }
702
704 [[nodiscard]] static std::unique_ptr<ICipher> create_metadata_cipher(
705 EncryptionAlgorithm /*algo*/, const std::vector<uint8_t>& key) {
706 // Metadata always uses GCM regardless of algorithm
707 return std::make_unique<AesGcmCipher>(key);
708 }
709};
710
711// ===========================================================================
712// Gap C-9: Power-on self-test (Known Answer Tests)
713//
714// NIST SP 800-140B / FIPS 140-3 §4.9.1 requires cryptographic modules to
715// perform known-answer tests (KATs) at initialization to verify algorithm
716// correctness. This function runs KATs for AES-256, AES-GCM, and AES-CTR
717// using NIST published test vectors.
718//
719// Call crypto_self_test() at application startup. Returns true if all KATs
720// pass. A false return indicates a broken build or hardware fault.
721//
722// References:
723// - NIST FIPS 197 Appendix C.3 (AES-256 single block)
724// - NIST SP 800-38D Test Case 16 (AES-256-GCM with AAD)
725// - NIST SP 800-38A F.5.5 (AES-256-CTR)
726// ===========================================================================
727
728namespace detail::kat {
729
731inline std::vector<uint8_t> hex_decode(const char* hex) {
732 std::vector<uint8_t> out;
733 while (*hex) {
734 auto nibble = [](char c) -> uint8_t {
735 if (c >= '0' && c <= '9') return static_cast<uint8_t>(c - '0');
736 if (c >= 'a' && c <= 'f') return static_cast<uint8_t>(c - 'a' + 10);
737 if (c >= 'A' && c <= 'F') return static_cast<uint8_t>(c - 'A' + 10);
738 return 0;
739 };
740 uint8_t hi = nibble(*hex++);
741 if (!*hex) break;
742 uint8_t lo = nibble(*hex++);
743 out.push_back(static_cast<uint8_t>((hi << 4) | lo));
744 }
745 return out;
746}
747
748} // namespace detail::kat
749
756[[nodiscard]] inline bool crypto_self_test() {
757 using namespace detail::kat;
758
759 // --- KAT 1: AES-256 single block (NIST FIPS 197 Appendix C.3) ---
760 {
761 auto key = hex_decode("000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f");
762 auto pt = hex_decode("00112233445566778899aabbccddeeff");
763 auto exp = hex_decode("8ea2b7ca516745bfeafc49904b496089");
764 if (key.size() != 32 || pt.size() != 16) return false;
765
766 Aes256 cipher(key.data());
767 uint8_t block[16];
768 std::memcpy(block, pt.data(), 16);
769 cipher.encrypt_block(block);
770 if (std::memcmp(block, exp.data(), 16) != 0) return false;
771 }
772
773 // --- KAT 2: AES-256-GCM (NIST SP 800-38D Test Case 16) ---
774 // CTR ciphertext matches NIST exactly. GHASH tag uses implementation-specific
775 // GF(2^128) bit ordering, so we verify CTR output + encrypt/decrypt roundtrip.
776 {
777 auto key = hex_decode("feffe9928665731c6d6a8f9467308308feffe9928665731c6d6a8f9467308308");
778 auto iv = hex_decode("cafebabefacedbaddecaf888");
779 auto aad = hex_decode("feedfacedeadbeeffeedfacedeadbeefabaddad2");
780 auto pt = hex_decode("d9313225f88406e5a55909c5aff5269a86a7a9531534f7da2e4c303d8a318a721c3c0c95956809532fcf0e2449a6b525b16aedf5aa0de657ba637b39");
781 auto exp_ct = hex_decode("522dc1f099567d07f47f37a32a84427d643a8cdcbfe5c0c97598a2bd2555d1aa8cb08e48590dbb3da7b08b1056828838c5f61e6393ba7a0abcc9f662");
782
783 AesGcm gcm(key.data());
784 auto result = gcm.encrypt(pt.data(), pt.size(), iv.data(), aad.data(), aad.size());
785 if (!result.has_value()) return false;
786 if (result->size() != pt.size() + 16) return false;
787 // CTR ciphertext matches NIST vector
788 if (std::memcmp(result->data(), exp_ct.data(), pt.size()) != 0) return false;
789 // Roundtrip: decrypt must recover original plaintext (verifies tag consistency)
790 auto dec = gcm.decrypt(result->data(), result->size(), iv.data(), aad.data(), aad.size());
791 if (!dec.has_value()) return false;
792 if (dec->size() != pt.size()) return false;
793 if (std::memcmp(dec->data(), pt.data(), pt.size()) != 0) return false;
794 }
795
796 // --- KAT 3: AES-256-CTR (NIST SP 800-38A F.5.5) ---
797 {
798 auto key = hex_decode("603deb1015ca71be2b73aef0857d77811f352c073b6108d72d9810a30914dff4");
799 auto iv = hex_decode("f0f1f2f3f4f5f6f7f8f9fafbfcfdfeff");
800 auto pt = hex_decode("6bc1bee22e409f96e93d7e117393172a");
801 auto exp = hex_decode("601ec313775789a5b7a7f504bbf3d228");
802
803 AesCtr ctr(key.data());
804 auto ct_result = ctr.encrypt(pt.data(), pt.size(), iv.data());
805 if (!ct_result.has_value()) return false;
806 auto& ct = *ct_result;
807 if (ct.size() != pt.size()) return false;
808 if (std::memcmp(ct.data(), exp.data(), pt.size()) != 0) return false;
809 }
810
811 return true;
812}
813
814} // namespace signet::forge::crypto
AES-256-CTR stream cipher implementation (NIST SP 800-38A).
AES-256-GCM authenticated encryption (NIST SP 800-38D).
AES-256 block cipher (FIPS-197).
Definition aes_core.hpp:253
void encrypt_block(uint8_t block[BLOCK_SIZE]) const
Encrypt a single 16-byte block in-place (FIPS-197 Section 5.1).
Definition aes_core.hpp:280
AES-256-CTR adapter – wraps the low-level AesCtr class behind ICipher.
size_t key_size() const noexcept override
expected< std::vector< uint8_t > > decrypt(const uint8_t *data, size_t size, const std::string &aad="") const override
Decrypt data produced by encrypt().
std::string_view algorithm_name() const noexcept override
AesCtrCipher(const uint8_t *key, size_t key_len)
Construct from a raw key pointer and length.
expected< std::vector< uint8_t > > encrypt(const uint8_t *data, size_t size, const std::string &aad="") const override
Encrypt data.
bool is_authenticated() const noexcept override
AesCtrCipher(const std::vector< uint8_t > &key)
Construct from a key vector (must be 32 bytes for AES-256).
~AesCtrCipher() override
Destructor: securely zeroes key material (CWE-244: heap inspection).
AES-256 in Counter Mode (CTR) as specified in NIST SP 800-38A.
Definition aes_ctr.hpp:68
expected< std::vector< uint8_t > > encrypt(const uint8_t *data, size_t size, const uint8_t iv[IV_SIZE]) const
Convenience alias for process() – encrypt data with AES-CTR.
Definition aes_ctr.hpp:162
AES-256-GCM adapter – wraps the low-level AesGcm class behind ICipher.
~AesGcmCipher() override
Destructor: securely zeroes key material (CWE-244: heap inspection).
std::function< void(uint64_t invocation_count)> RotationCallback
Callback type for key rotation notification.
bool is_authenticated() const noexcept override
Whether this cipher provides authentication (GCM=true, CTR=false).
size_t key_size() const noexcept override
Key size in bytes (32 for AES-256).
expected< std::vector< uint8_t > > encrypt(const uint8_t *data, size_t size, const std::string &aad="") const override
Encrypt data.
expected< std::vector< uint8_t > > decrypt(const uint8_t *data, size_t size, const std::string &aad="") const override
Decrypt data produced by encrypt().
uint64_t invocation_count() const noexcept
Get the current number of encrypt() invocations on this key.
AesGcmCipher(const uint8_t *key, size_t key_len)
Construct from a raw key pointer and length.
AesGcmCipher(const std::vector< uint8_t > &key)
Construct from a key vector (must be 32 bytes for AES-256).
std::string_view algorithm_name() const noexcept override
Human-readable algorithm name.
void set_rotation_callback(RotationCallback cb, uint64_t threshold=DEFAULT_ROTATION_THRESHOLD)
Register a callback invoked when the key approaches its invocation limit.
AES-256 in Galois/Counter Mode (GCM) as specified in NIST SP 800-38D.
Definition aes_gcm.hpp:406
expected< std::vector< uint8_t > > encrypt(const uint8_t *plaintext, size_t plaintext_size, const uint8_t iv[IV_SIZE], const uint8_t *aad=nullptr, size_t aad_size=0) const
Authenticated encryption with additional data (AEAD).
Definition aes_gcm.hpp:487
expected< std::vector< uint8_t > > decrypt(const uint8_t *ciphertext_with_tag, size_t total_size, const uint8_t iv[IV_SIZE], const uint8_t *aad=nullptr, size_t aad_size=0) const
Authenticated decryption and verification (NIST SP 800-38D Section 7.2).
Definition aes_gcm.hpp:592
Abstract cipher interface — unified API for authenticated (GCM) and unauthenticated (CTR) encryption.
virtual size_t key_size() const noexcept=0
Key size in bytes (32 for AES-256).
virtual std::string_view algorithm_name() const noexcept=0
Human-readable algorithm name.
virtual expected< std::vector< uint8_t > > decrypt(const uint8_t *data, size_t size, const std::string &aad="") const =0
Decrypt data produced by encrypt().
virtual expected< std::vector< uint8_t > > encrypt(const uint8_t *data, size_t size, const std::string &aad="") const =0
Encrypt data.
virtual bool is_authenticated() const noexcept=0
Whether this cipher provides authentication (GCM=true, CTR=false).
RAII container for sensitive key material with mlock and secure zeroization.
SecureKeyBuffer(SecureKeyBuffer &&other) noexcept
SecureKeyBuffer(const SecureKeyBuffer &)=delete
SecureKeyBuffer(const std::vector< uint8_t > &key)
Construct from existing key bytes (copies and locks).
SecureKeyBuffer & operator=(const SecureKeyBuffer &)=delete
SecureKeyBuffer & operator=(SecureKeyBuffer &&other) noexcept
SecureKeyBuffer(const uint8_t *ptr, size_t size)
Construct from raw bytes (copies and locks).
SecureKeyBuffer(size_t size)
Construct with a specified size of random key material.
A lightweight result type that holds either a success value of type T or an Error.
Definition error.hpp:145
HKDF key derivation (RFC 5869) using HMAC-SHA256.
Key material, encryption configuration, and TLV serialization for Parquet Modular Encryption (PME).
std::vector< uint8_t > generate_iv(size_t iv_size)
Generate a random initialization vector of the specified size.
void fill_random_bytes(uint8_t *buf, size_t size)
Fill a buffer with cryptographically random bytes using the best available OS-level CSPRNG (CWE-338: ...
expected< IvParsed > parse_iv_header(const uint8_t *data, size_t size)
Parse the IV header from encrypted data: [1 byte: iv_size] [iv] [ciphertext].
std::vector< uint8_t > prepend_iv(const std::vector< uint8_t > &iv, const std::vector< uint8_t > &ciphertext)
Prepend an IV header to ciphertext: [1 byte: iv.size()] [iv bytes] [ciphertext].
void fill_random_bytes_tested(CrngtState &state, uint8_t *buf, size_t size)
Generate random bytes with FIPS 140-3 §4.9.2 continuous test.
std::vector< uint8_t > hex_decode(const char *hex)
Decode a hex string to bytes (internal helper for KAT vectors).
bool lock_memory(void *ptr, size_t size)
Lock a memory region so it is not paged to swap.
void unlock_memory(void *ptr, size_t size)
Unlock a previously locked memory region.
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 crypto_self_test()
Run power-on self-tests (Known Answer Tests) for all crypto primitives.
EncryptionAlgorithm
Encryption algorithm identifier.
@ ENCRYPTION_ERROR
An encryption or decryption operation failed (bad key, tampered ciphertext, PME error).
Lightweight error value carrying an ErrorCode and a human-readable message.
Definition error.hpp:101
Factory for creating cipher instances from algorithm enum + raw key.
static std::unique_ptr< ICipher > create_footer_cipher(EncryptionAlgorithm, const std::vector< uint8_t > &key)
Create a footer cipher (always authenticated = GCM).
static std::unique_ptr< ICipher > create_metadata_cipher(EncryptionAlgorithm, const std::vector< uint8_t > &key)
Create a metadata cipher (always authenticated = GCM).
static std::unique_ptr< ICipher > create_column_cipher(EncryptionAlgorithm algo, const std::vector< uint8_t > &key)
Create a column data cipher (GCM or CTR based on algorithm).
Result of parsing an IV header from encrypted data.
const uint8_t * ciphertext
Pointer to the ciphertext after the IV.
const uint8_t * iv
Pointer to the IV bytes within the input buffer.
size_t ct_size
Ciphertext length (may include GCM auth tag).
CRNGT state — stores the previous 32-byte RNG output for comparison.