Skip to content

Commit 9f31956

Browse files
authored
Merge pull request #185 from permanence-ai/claude-logging-pr-a
Add pluggable logging hook to safe-crypto-lib (issue #184, PR A)
2 parents 94c6598 + 4519e82 commit 9f31956

12 files changed

Lines changed: 604 additions & 41 deletions

File tree

‎safe-crypto-lib/CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ target_sources(${LIB_NAME} INTERFACE
88
asymmetric.hpp
99
contracts.hpp
1010
crypto_error.hpp
11+
crypto_log.hpp
1112
crypto_provider.hpp
1213
defs.hpp
1314
digests.hpp

‎safe-crypto-lib/aead.hpp‎

Lines changed: 86 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,10 @@
55
#include <cstddef>
66
#include <expected>
77
#include <optional>
8+
#include <string>
89

910
#include "crypto_error.hpp"
11+
#include "crypto_log.hpp"
1012
#include "defs.hpp"
1113
#include "psa_backend.hpp"
1214
#include "random.hpp"
@@ -213,7 +215,20 @@ auto aes256_gcm_encrypt(
213215
const std::optional<SecureBuffer>& aad = std::nullopt)
214216
-> std::expected<AesGcmResult, CryptoError>
215217
{
216-
return aes256_gcm_encrypt_impl<DefaultProvider>(key, plaintext, aad);
218+
if (crypto_log_enabled(CryptoLogLevel::Debug)) {
219+
crypto_log(CryptoLogLevel::Debug,
220+
crypto_log_detail::msg("aes256_gcm_encrypt",
221+
"input", plaintext.size(),
222+
"aad", aad.has_value() ? aad->size() : 0U));
223+
}
224+
auto result = aes256_gcm_encrypt_impl<DefaultProvider>(key, plaintext, aad);
225+
if (!result.has_value()) {
226+
crypto_log(CryptoLogLevel::Error, "aes256_gcm_encrypt: " + result.error().message());
227+
} else if (crypto_log_enabled(CryptoLogLevel::Debug)) {
228+
crypto_log(CryptoLogLevel::Debug,
229+
crypto_log_detail::msg("aes256_gcm_encrypt", "ciphertext", result->ciphertext.size()));
230+
}
231+
return result;
217232
}
218233

219234
[[nodiscard]]
@@ -223,7 +238,20 @@ inline auto aes256_gcm_decrypt(
223238
const std::optional<SecureBuffer>& aad = std::nullopt)
224239
-> std::expected<SecureBuffer, CryptoError>
225240
{
226-
return aes256_gcm_decrypt_impl<DefaultProvider>(key, ciphertext, aad);
241+
if (crypto_log_enabled(CryptoLogLevel::Debug)) {
242+
crypto_log(CryptoLogLevel::Debug,
243+
crypto_log_detail::msg("aes256_gcm_decrypt",
244+
"ciphertext", ciphertext.ciphertext.size(),
245+
"aad", aad.has_value() ? aad->size() : 0U));
246+
}
247+
auto result = aes256_gcm_decrypt_impl<DefaultProvider>(key, ciphertext, aad);
248+
if (!result.has_value()) {
249+
crypto_log(CryptoLogLevel::Error, "aes256_gcm_decrypt: " + result.error().message());
250+
} else if (crypto_log_enabled(CryptoLogLevel::Debug)) {
251+
crypto_log(CryptoLogLevel::Debug,
252+
crypto_log_detail::msg("aes256_gcm_decrypt", "plaintext", result->size()));
253+
}
254+
return result;
227255
}
228256

229257
template<SecureBufferLike Plaintext>
@@ -234,7 +262,20 @@ auto chacha20_poly1305_encrypt(
234262
const std::optional<SecureBuffer>& aad = std::nullopt)
235263
-> std::expected<ChaCha20Poly1305Result, CryptoError>
236264
{
237-
return chacha20_poly1305_encrypt_impl<DefaultProvider>(key, plaintext, aad);
265+
if (crypto_log_enabled(CryptoLogLevel::Debug)) {
266+
crypto_log(CryptoLogLevel::Debug,
267+
crypto_log_detail::msg("chacha20_poly1305_encrypt",
268+
"input", plaintext.size(),
269+
"aad", aad.has_value() ? aad->size() : 0U));
270+
}
271+
auto result = chacha20_poly1305_encrypt_impl<DefaultProvider>(key, plaintext, aad);
272+
if (!result.has_value()) {
273+
crypto_log(CryptoLogLevel::Error, "chacha20_poly1305_encrypt: " + result.error().message());
274+
} else if (crypto_log_enabled(CryptoLogLevel::Debug)) {
275+
crypto_log(CryptoLogLevel::Debug,
276+
crypto_log_detail::msg("chacha20_poly1305_encrypt", "ciphertext", result->ciphertext.size()));
277+
}
278+
return result;
238279
}
239280

240281
[[nodiscard]]
@@ -244,7 +285,20 @@ inline auto chacha20_poly1305_decrypt(
244285
const std::optional<SecureBuffer>& aad = std::nullopt)
245286
-> std::expected<SecureBuffer, CryptoError>
246287
{
247-
return chacha20_poly1305_decrypt_impl<DefaultProvider>(key, ciphertext, aad);
288+
if (crypto_log_enabled(CryptoLogLevel::Debug)) {
289+
crypto_log(CryptoLogLevel::Debug,
290+
crypto_log_detail::msg("chacha20_poly1305_decrypt",
291+
"ciphertext", ciphertext.ciphertext.size(),
292+
"aad", aad.has_value() ? aad->size() : 0U));
293+
}
294+
auto result = chacha20_poly1305_decrypt_impl<DefaultProvider>(key, ciphertext, aad);
295+
if (!result.has_value()) {
296+
crypto_log(CryptoLogLevel::Error, "chacha20_poly1305_decrypt: " + result.error().message());
297+
} else if (crypto_log_enabled(CryptoLogLevel::Debug)) {
298+
crypto_log(CryptoLogLevel::Debug,
299+
crypto_log_detail::msg("chacha20_poly1305_decrypt", "plaintext", result->size()));
300+
}
301+
return result;
248302
}
249303

250304

@@ -259,7 +313,20 @@ auto symmetric_encrypt(
259313
const std::optional<SecureBuffer>& aad = std::nullopt)
260314
-> std::expected<AesGcmResult, CryptoError>
261315
{
262-
return aes256_gcm_encrypt_impl<DefaultProvider>(key, plaintext, aad);
316+
if (crypto_log_enabled(CryptoLogLevel::Debug)) {
317+
crypto_log(CryptoLogLevel::Debug,
318+
crypto_log_detail::msg("symmetric_encrypt",
319+
"input", plaintext.size(),
320+
"aad", aad.has_value() ? aad->size() : 0U));
321+
}
322+
auto result = aes256_gcm_encrypt_impl<DefaultProvider>(key, plaintext, aad);
323+
if (!result.has_value()) {
324+
crypto_log(CryptoLogLevel::Error, "symmetric_encrypt: " + result.error().message());
325+
} else if (crypto_log_enabled(CryptoLogLevel::Debug)) {
326+
crypto_log(CryptoLogLevel::Debug,
327+
crypto_log_detail::msg("symmetric_encrypt", "ciphertext", result->ciphertext.size()));
328+
}
329+
return result;
263330
}
264331

265332
[[nodiscard]]
@@ -269,5 +336,18 @@ inline auto symmetric_decrypt(
269336
const std::optional<SecureBuffer>& aad = std::nullopt)
270337
-> std::expected<SecureBuffer, CryptoError>
271338
{
272-
return aes256_gcm_decrypt_impl<DefaultProvider>(key, ciphertext, aad);
339+
if (crypto_log_enabled(CryptoLogLevel::Debug)) {
340+
crypto_log(CryptoLogLevel::Debug,
341+
crypto_log_detail::msg("symmetric_decrypt",
342+
"ciphertext", ciphertext.ciphertext.size(),
343+
"aad", aad.has_value() ? aad->size() : 0U));
344+
}
345+
auto result = aes256_gcm_decrypt_impl<DefaultProvider>(key, ciphertext, aad);
346+
if (!result.has_value()) {
347+
crypto_log(CryptoLogLevel::Error, "symmetric_decrypt: " + result.error().message());
348+
} else if (crypto_log_enabled(CryptoLogLevel::Debug)) {
349+
crypto_log(CryptoLogLevel::Debug,
350+
crypto_log_detail::msg("symmetric_decrypt", "plaintext", result->size()));
351+
}
352+
return result;
273353
}

‎safe-crypto-lib/asymmetric.hpp‎

Lines changed: 49 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
#include <optional>
99

1010
#include "crypto_error.hpp"
11+
#include "crypto_log.hpp"
1112
#include "psa_backend.hpp"
1213
#include "secure_buffer.hpp"
1314

@@ -216,7 +217,18 @@ auto rsa_oaep_encrypt(
216217
const std::optional<SecureBuffer>& label = std::nullopt)
217218
-> std::expected<SecureBuffer, CryptoError>
218219
{
219-
return rsa_oaep_encrypt_impl<KB, DefaultProvider>(public_key, plaintext, label);
220+
if (crypto_log_enabled(CryptoLogLevel::Debug)) {
221+
crypto_log(CryptoLogLevel::Debug,
222+
crypto_log_detail::msg("rsa_oaep_encrypt", "plaintext", plaintext.size()));
223+
}
224+
auto result = rsa_oaep_encrypt_impl<KB, DefaultProvider>(public_key, plaintext, label);
225+
if (!result.has_value()) {
226+
crypto_log(CryptoLogLevel::Error, "rsa_oaep_encrypt: " + result.error().message());
227+
} else if (crypto_log_enabled(CryptoLogLevel::Debug)) {
228+
crypto_log(CryptoLogLevel::Debug,
229+
crypto_log_detail::msg("rsa_oaep_encrypt", "ciphertext", result->size()));
230+
}
231+
return result;
220232
}
221233

222234
template<RsaKeyBits KB, SecureBufferLike Ciphertext>
@@ -227,7 +239,18 @@ auto rsa_oaep_decrypt(
227239
const std::optional<SecureBuffer>& label = std::nullopt)
228240
-> std::expected<SecureBuffer, CryptoError>
229241
{
230-
return rsa_oaep_decrypt_impl<KB, DefaultProvider>(key_pair, ciphertext, label);
242+
if (crypto_log_enabled(CryptoLogLevel::Debug)) {
243+
crypto_log(CryptoLogLevel::Debug,
244+
crypto_log_detail::msg("rsa_oaep_decrypt", "ciphertext", ciphertext.size()));
245+
}
246+
auto result = rsa_oaep_decrypt_impl<KB, DefaultProvider>(key_pair, ciphertext, label);
247+
if (!result.has_value()) {
248+
crypto_log(CryptoLogLevel::Error, "rsa_oaep_decrypt: " + result.error().message());
249+
} else if (crypto_log_enabled(CryptoLogLevel::Debug)) {
250+
crypto_log(CryptoLogLevel::Debug,
251+
crypto_log_detail::msg("rsa_oaep_decrypt", "plaintext", result->size()));
252+
}
253+
return result;
231254
}
232255

233256
template<RsaKeyBits KB, SecureBufferLike Message>
@@ -237,7 +260,18 @@ auto rsa_pss_sign(
237260
const Message& message)
238261
-> std::expected<SecureBuffer, CryptoError>
239262
{
240-
return rsa_pss_sign_impl<KB, DefaultProvider>(key_pair, message);
263+
if (crypto_log_enabled(CryptoLogLevel::Debug)) {
264+
crypto_log(CryptoLogLevel::Debug,
265+
crypto_log_detail::msg("rsa_pss_sign", "msg", message.size()));
266+
}
267+
auto result = rsa_pss_sign_impl<KB, DefaultProvider>(key_pair, message);
268+
if (!result.has_value()) {
269+
crypto_log(CryptoLogLevel::Error, "rsa_pss_sign: " + result.error().message());
270+
} else if (crypto_log_enabled(CryptoLogLevel::Debug)) {
271+
crypto_log(CryptoLogLevel::Debug,
272+
crypto_log_detail::msg("rsa_pss_sign", "sig", result->size()));
273+
}
274+
return result;
241275
}
242276

243277
template<RsaKeyBits KB, SecureBufferLike Message, SecureBufferLike Signature>
@@ -248,5 +282,16 @@ auto rsa_pss_verify(
248282
const Signature& signature)
249283
-> std::expected<bool, CryptoError>
250284
{
251-
return rsa_pss_verify_impl<KB, DefaultProvider>(public_key, message, signature);
285+
if (crypto_log_enabled(CryptoLogLevel::Debug)) {
286+
crypto_log(CryptoLogLevel::Debug,
287+
crypto_log_detail::msg("rsa_pss_verify", "msg", message.size(), "sig", signature.size()));
288+
}
289+
auto result = rsa_pss_verify_impl<KB, DefaultProvider>(public_key, message, signature);
290+
if (!result.has_value()) {
291+
crypto_log(CryptoLogLevel::Error, "rsa_pss_verify: " + result.error().message());
292+
} else if (crypto_log_enabled(CryptoLogLevel::Debug)) {
293+
crypto_log(CryptoLogLevel::Debug,
294+
result.value() ? "rsa_pss_verify: ok" : "rsa_pss_verify: mismatch");
295+
}
296+
return result;
252297
}

‎safe-crypto-lib/crypto_log.hpp‎

Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
// SPDX-License-Identifier: Apache-2.0
2+
3+
#pragma once
4+
5+
// Zero-dependency pluggable logging hook for safe-crypto-lib.
6+
//
7+
// Usage:
8+
// crypto_set_log_sink([](CryptoLogLevel lvl, std::string_view msg) {
9+
// my_logger.log(lvl, msg);
10+
// }, CryptoLogLevel::Debug);
11+
//
12+
// The library emits Debug messages at operation entry and success,
13+
// and Error messages at every failure return. Key material, IVs,
14+
// and payload bytes are never included in log messages.
15+
//
16+
// Default state: no sink registered, all logging is a no-op.
17+
// Thread-safe: crypto_set_log_sink is safe to call once before spawning
18+
// worker threads. crypto_log uses a mutex for the fn pointer and a
19+
// separate atomic for the fast-path threshold check.
20+
21+
#include <atomic>
22+
#include <cstddef>
23+
#include <functional>
24+
#include <mutex>
25+
#include <string>
26+
#include <string_view>
27+
28+
29+
enum class CryptoLogLevel : int {
30+
Debug = 0,
31+
Info = 1,
32+
Warn = 2,
33+
Error = 3,
34+
Off = 4,
35+
};
36+
37+
using CryptoLogSink = std::function<void(CryptoLogLevel, std::string_view)>;
38+
39+
40+
namespace crypto_log_detail {
41+
42+
// Build "op: key=val[, key=val...]" strings without <format> or {fmt}.
43+
// Returns a std::string so callers can wrap the call in crypto_log_enabled guard.
44+
inline auto msg(std::string_view op, std::string_view k1, std::size_t v1) -> std::string {
45+
return std::string(op) + ": " + std::string(k1) + "=" + std::to_string(v1) + " bytes";
46+
}
47+
inline auto msg(std::string_view op, std::string_view k1, std::size_t v1,
48+
std::string_view k2, std::size_t v2) -> std::string {
49+
return std::string(op) + ": " + std::string(k1) + "=" + std::to_string(v1)
50+
+ " bytes, " + std::string(k2) + "=" + std::to_string(v2) + " bytes";
51+
}
52+
inline auto msg(std::string_view op, std::string_view k1, std::size_t v1,
53+
std::string_view k2, std::size_t v2,
54+
std::string_view k3, std::size_t v3) -> std::string {
55+
return std::string(op) + ": " + std::string(k1) + "=" + std::to_string(v1)
56+
+ " bytes, " + std::string(k2) + "=" + std::to_string(v2)
57+
+ " bytes, " + std::string(k3) + "=" + std::to_string(v3) + " bytes";
58+
}
59+
60+
struct LogState {
61+
std::mutex mu;
62+
CryptoLogSink fn;
63+
std::atomic<int> threshold{static_cast<int>(CryptoLogLevel::Off)};
64+
};
65+
66+
inline auto state() noexcept -> LogState& {
67+
static LogState s; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
68+
return s;
69+
}
70+
71+
} // namespace crypto_log_detail
72+
73+
74+
// Register a sink callable and a minimum log level.
75+
// Passing nullptr (or an empty function) disables logging.
76+
// Intended to be called once at startup before any library operations.
77+
inline void crypto_set_log_sink(CryptoLogSink sink,
78+
CryptoLogLevel threshold = CryptoLogLevel::Debug)
79+
{
80+
auto& s = crypto_log_detail::state();
81+
const int thr = sink ? static_cast<int>(threshold)
82+
: static_cast<int>(CryptoLogLevel::Off);
83+
{
84+
std::lock_guard lock(s.mu);
85+
s.fn = std::move(sink);
86+
}
87+
// Store threshold after fn is visible so threads that observe threshold != Off
88+
// will always find a valid fn under the mutex.
89+
s.threshold.store(thr, std::memory_order_release);
90+
}
91+
92+
// Fast predicate for guarding dynamic message construction.
93+
// Wrapping expensive formatting in: if (crypto_log_enabled(L)) { crypto_log(L, msg()); }
94+
// avoids string allocation on the hot path when logging is off.
95+
[[nodiscard]]
96+
inline bool crypto_log_enabled(CryptoLogLevel level) noexcept {
97+
return crypto_log_detail::state().threshold.load(std::memory_order_relaxed)
98+
<= static_cast<int>(level);
99+
}
100+
101+
// Emit a log message. No-op if no sink is registered or level < threshold.
102+
// String literals can be passed without the crypto_log_enabled guard since
103+
// no allocation occurs and the threshold check inside is a single atomic load.
104+
inline void crypto_log(CryptoLogLevel level, std::string_view msg) {
105+
auto& s = crypto_log_detail::state();
106+
if (s.threshold.load(std::memory_order_acquire) > static_cast<int>(level)) { return; }
107+
std::lock_guard lock(s.mu);
108+
if (s.fn) { s.fn(level, msg); }
109+
}

‎safe-crypto-lib/digests.hpp‎

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
#include <expected>
88

99
#include "crypto_error.hpp"
10+
#include "crypto_log.hpp"
1011
#include "psa_backend.hpp"
1112
#include "secure_buffer.hpp"
1213
#include "sha_variant.hpp"
@@ -44,5 +45,16 @@ template<ShaVariant V, SecureBufferLike Input>
4445
auto sha(const Input& input)
4546
-> std::expected<FixedSecureBuffer<sha_output_size(V)>, CryptoError>
4647
{
47-
return sha_impl<V, DefaultProvider>(input);
48+
if (crypto_log_enabled(CryptoLogLevel::Debug)) {
49+
crypto_log(CryptoLogLevel::Debug,
50+
crypto_log_detail::msg("sha", "input", input.size()));
51+
}
52+
auto result = sha_impl<V, DefaultProvider>(input);
53+
if (!result.has_value()) {
54+
crypto_log(CryptoLogLevel::Error, "sha: " + result.error().message());
55+
} else if (crypto_log_enabled(CryptoLogLevel::Debug)) {
56+
crypto_log(CryptoLogLevel::Debug,
57+
crypto_log_detail::msg("sha", "digest", sha_output_size(V)));
58+
}
59+
return result;
4860
}

0 commit comments

Comments
 (0)