Files
voice-cat/core/src/crypto/crypto.h

145 lines
5.6 KiB
C
Raw Normal View History

/*
* crypto/crypto.h TLS 1.3 (mbedTLS) and the media AEAD (libsodium).
*
* Design: docs/security.md. Control channel = TLS 1.3. Media = keys exported from the TLS
* session (RFC 5705 / 8446) + per-frame ChaCha20-Poly1305 with a counter nonce and a
* sliding-window replay filter. Encryption is MANDATORY never add a plaintext path.
*/
#ifndef VOICECAT_CRYPTO_CRYPTO_H
#define VOICECAT_CRYPTO_CRYPTO_H
#include <cstddef>
#include <cstdint>
2026-06-15 23:48:44 +02:00
#include <string>
#ifdef VOICECAT_HAS_NET
#include <array>
#include <filesystem>
#include <functional>
#include <memory>
// libsodium
#include <sodium.h>
// mbedTLS
#include <mbedtls/ctr_drbg.h>
#include <mbedtls/entropy.h>
#include <mbedtls/net_sockets.h>
#include <mbedtls/pk.h>
#include <mbedtls/ssl.h>
#include <mbedtls/x509_crt.h>
namespace voicecat::crypto {
2026-06-15 23:48:44 +02:00
// ── Server identity ────────────────────────────────────────────────────────────
// Long-lived Ed25519 key identifying this server instance across cert rotations.
// Fingerprint is the 32-byte SHA-256 of the public key.
struct ServerIdentity {
std::array<uint8_t, crypto_sign_ed25519_PUBLICKEYBYTES> pk{};
std::array<uint8_t, crypto_sign_ed25519_SECRETKEYBYTES> sk{};
std::array<uint8_t, 32> fingerprint{};
static ServerIdentity generate();
static ServerIdentity load(const std::filesystem::path& path);
void save(const std::filesystem::path& path) const;
std::string fingerprint_hex() const;
};
// ── Server TLS certificate ─────────────────────────────────────────────────────
// Self-signed ECDSA-P256 cert for TLS. On first run, generated and persisted.
struct ServerCert {
std::string pem_cert;
std::string pem_key;
static ServerCert generate(const std::string& server_name);
static ServerCert load(const std::filesystem::path& cert_path,
const std::filesystem::path& key_path);
void save(const std::filesystem::path& cert_path,
const std::filesystem::path& key_path) const;
};
// ── TLS 1.3 context ───────────────────────────────────────────────────────────
// Wraps mbedTLS for one TLS connection (server or client side).
// All public methods except close() must be called from a single thread at a time.
class TlsContext {
public:
2026-06-15 23:48:44 +02:00
enum class Role { Server, Client };
// server_cert: required for server role; nullptr for client
// pinned_fp: 32-byte Ed25519 fingerprint to accept (client TOFU); nullptr = any
TlsContext(Role role, const ServerCert* server_cert,
const std::array<uint8_t, 32>* pinned_fp = nullptr);
~TlsContext();
TlsContext(const TlsContext&) = delete;
TlsContext& operator=(const TlsContext&) = delete;
// Perform the TLS handshake over an already-connected BSD socket fd.
// Blocking — run from a WorkerPool thread.
// Returns true on success; error contains a diagnostic string on failure.
bool handshake(int socket_fd, std::string& error);
// Read/write post-handshake (single-threaded). Returns bytes transferred, or <0 on error.
int read(uint8_t* buf, size_t len);
int write(const uint8_t* buf, size_t len);
// RFC 5705 / RFC 8446 §7.5 exporter — derive media keys after handshake.
bool export_keying_material(const char* label, const uint8_t* ctx, size_t ctx_len,
uint8_t* out, size_t out_len);
// Whether the handshake completed.
bool ready() const { return ready_; }
// Underlying socket fd (valid after handshake). For select() in the caller.
int native_fd() const { return net_ctx_.fd; }
// Set per-read timeout (ms, 0 = blocking). Affects post-handshake reads.
void set_read_timeout(uint32_t ms);
// True when the given return value from read() indicates a read timeout.
static bool is_timeout_error(int rc);
private:
Role role_;
const std::array<uint8_t, 32>* pinned_fp_;
bool ready_{false};
mbedtls_entropy_context entropy_{};
mbedtls_ctr_drbg_context ctr_drbg_{};
mbedtls_ssl_context ssl_{};
mbedtls_ssl_config conf_{};
mbedtls_x509_crt srvcert_{};
mbedtls_pk_context pkey_{};
mbedtls_net_context net_ctx_{};
};
2026-06-15 23:48:44 +02:00
// ── Media AEAD (M2) ───────────────────────────────────────────────────────────
// Per-frame voice encryption. Abstracted so the backend is swappable.
class MediaCrypto {
public:
virtual ~MediaCrypto() = default;
virtual long seal(const uint8_t* plain, size_t len, const uint8_t* aad, size_t aad_len,
uint8_t* out, size_t out_cap) = 0;
virtual long open(const uint8_t* sealed, size_t len, const uint8_t* aad, size_t aad_len,
uint8_t* out, size_t out_cap) = 0;
};
} // namespace voicecat::crypto
2026-06-15 23:48:44 +02:00
#else // !VOICECAT_HAS_NET — skeleton stubs
namespace voicecat::crypto {
class MediaCrypto {
public:
virtual ~MediaCrypto() = default;
virtual long seal(const uint8_t*, size_t, const uint8_t*, size_t, uint8_t*, size_t) = 0;
virtual long open(const uint8_t*, size_t, const uint8_t*, size_t, uint8_t*, size_t) = 0;
};
} // namespace voicecat::crypto
#endif // VOICECAT_HAS_NET
#endif // VOICECAT_CRYPTO_CRYPTO_H