Implements the full M1 milestone. Two clients authenticate over TLS 1.3 (guest + Argon2id password) and exchange channel + private text messages through a real server. All five ctest --preset m1-dev tests pass in ~1 s. Key components added: - vcpkg baseline + m1-dev preset (protobuf/mbedTLS/libsodium/asio/sqlite3) - FrameCodec feed+emit, encode/decode_envelope, protobuf codegen - TcpServerConn with blocking TLS handshake thread + tls_read_loop - TlsContext (mbedTLS 1.3, ECDSA-P256 self-signed cert, TOFU on client) - WorkerPool (3 threads, used for Argon2id) - Database: SQLite + libsodium Argon2id, account lifecycle, bootstrap admin - ServerIdentityManager: Ed25519 key + cert generate/persist/fingerprint - ConnSession state machine: WaitingHello -> WaitingAuth -> Authenticated - SessionRegistry: channel tree, user map, text routing, broadcast - vc_client full M1 C ABI: connect/TLS/handshake/auth/text/disconnect - voicecat-admin CLI: account add/reset/del/list - test_m1_integration: M1 exit criterion, verified green Bug fixed: double-framing in ConnSession::send_envelope -- encode_envelope was adding the [4-byte len] prefix, then TcpServerConn::send_frame added a second one, causing the client to parse [len][proto] as protobuf (silent failure). Fixed by serializing raw protobuf bytes in send_envelope and letting send_frame apply the single length prefix. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
145 lines
5.6 KiB
C++
145 lines
5.6 KiB
C++
/*
|
|
* 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>
|
|
#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 {
|
|
|
|
// ── 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:
|
|
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_{};
|
|
};
|
|
|
|
// ── 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
|
|
|
|
#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
|