Add allocation-free C++ stream cipher codecs

src/crypto/stream.hpp implements the legacy stream methods this build
supports (AES CFB/CTR, Camellia CFB, Salsa20, ChaCha20, ChaCha20-IETF)
as value types. The AES and Camellia contexts are held by value through
Mbed TLS's block-cipher APIs instead of an allocated
mbedtls_cipher_context_t, and the libsodium ciphers track their
keystream position, finishing partial 64-byte blocks from a stack block
instead of the padding copies stream.cpp makes.

test_crypto_stream_cpp runs TCP streams (with fragments that split
keystream blocks) and UDP packets between the C and C++
implementations in both directions for all twelve methods and checks
IV replay; alloc_steady_crypto_stream requires zero allocations for new
connections and packets.
This commit is contained in:
Max Lv
2026-10-14 22:53:07 +08:00
parent ed75faf80f
commit d91ef14f76
4 changed files with 715 additions and 0 deletions
+287
View File
@@ -0,0 +1,287 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#include "crypto/stream.hpp"
#include <algorithm>
#include <cstring>
#include <sodium.h>
#include "crypto.h"
// crypto.h defines min/max macros for the C sources.
#undef min
#undef max
namespace ss::crypto {
namespace {
constexpr StreamSpec kStreams[] = {
{ "aes-128-cfb", StreamKind::aes_cfb, 16, 16 },
{ "aes-192-cfb", StreamKind::aes_cfb, 24, 16 },
{ "aes-256-cfb", StreamKind::aes_cfb, 32, 16 },
{ "aes-128-ctr", StreamKind::aes_ctr, 16, 16 },
{ "aes-192-ctr", StreamKind::aes_ctr, 24, 16 },
{ "aes-256-ctr", StreamKind::aes_ctr, 32, 16 },
{ "camellia-128-cfb", StreamKind::camellia_cfb, 16, 16 },
{ "camellia-192-cfb", StreamKind::camellia_cfb, 24, 16 },
{ "camellia-256-cfb", StreamKind::camellia_cfb, 32, 16 },
{ "salsa20", StreamKind::salsa20, 32, 8 },
{ "chacha20", StreamKind::chacha20, 32, 8 },
{ "chacha20-ietf", StreamKind::chacha20_ietf, 32, 12 },
};
constexpr std::size_t kSodiumBlock = 64;
int sodium_xor(StreamKind kind, std::uint8_t *out, const std::uint8_t *in, std::size_t n, const std::uint8_t *nonce,
std::uint64_t block, const std::uint8_t *key) noexcept
{
switch (kind) {
case StreamKind::salsa20:
return crypto_stream_salsa20_xor_ic(out, in, n, nonce, block, key);
case StreamKind::chacha20:
return crypto_stream_chacha20_xor_ic(out, in, n, nonce, block, key);
case StreamKind::chacha20_ietf:
return crypto_stream_chacha20_ietf_xor_ic(out, in, n, nonce, static_cast<std::uint32_t>(block), key);
default:
return -1;
}
}
} // namespace
std::optional<StreamSpec> find_stream(std::string_view name) noexcept
{
for (const StreamSpec &spec : kStreams) {
if (spec.name == name) {
return spec;
}
}
return std::nullopt;
}
Result<StreamKey> StreamKey::from_password(const StreamSpec &spec, const char *password) noexcept
{
if (password == nullptr) {
return fail(Errc::config);
}
StreamKey key{ spec, {} };
if (crypto_derive_key(password, key.bytes.data(), spec.key_len) != spec.key_len) {
return fail(Errc::config);
}
return key;
}
StreamCipher::Sodium::~Sodium()
{
sodium_memzero(key.data(), key.size());
}
Status StreamCipher::init(const StreamKey &key, std::span<const std::uint8_t> iv, bool encrypt) noexcept
{
if (iv.size() != key.spec.iv_len) {
return fail(Errc::invalid);
}
const unsigned bits = key.spec.key_len * 8U;
switch (key.spec.kind) {
case StreamKind::aes_cfb:
case StreamKind::aes_ctr: {
auto &aes = impl_.emplace<Aes>();
// CFB and CTR only ever run the block cipher forwards.
if (mbedtls_aes_setkey_enc(&aes.ctx, key.bytes.data(), bits) != 0) {
return fail(Errc::crypto);
}
std::memcpy(aes.iv.data(), iv.data(), 16);
aes.ctr = key.spec.kind == StreamKind::aes_ctr;
aes.mode = encrypt ? MBEDTLS_AES_ENCRYPT : MBEDTLS_AES_DECRYPT;
return {};
}
case StreamKind::camellia_cfb: {
auto &cam = impl_.emplace<Camellia>();
if (mbedtls_camellia_setkey_enc(&cam.ctx, key.bytes.data(), bits) != 0) {
return fail(Errc::crypto);
}
std::memcpy(cam.iv.data(), iv.data(), 16);
cam.mode = encrypt ? MBEDTLS_CAMELLIA_ENCRYPT : MBEDTLS_CAMELLIA_DECRYPT;
return {};
}
case StreamKind::salsa20:
case StreamKind::chacha20:
case StreamKind::chacha20_ietf: {
auto &s = impl_.emplace<Sodium>();
s.kind = key.spec.kind;
std::memcpy(s.key.data(), key.bytes.data(), key.spec.key_len);
std::memcpy(s.nonce.data(), iv.data(), iv.size());
s.position = 0;
return {};
}
}
return fail(Errc::invalid);
}
void StreamCipher::reset() noexcept
{
impl_.emplace<std::monostate>();
}
Status StreamCipher::apply(const std::uint8_t *in, std::uint8_t *out, std::size_t n) noexcept
{
if (n == 0) {
return {};
}
int rc = -1;
if (auto *aes = std::get_if<Aes>(&impl_)) {
if (aes->ctr) {
rc = mbedtls_aes_crypt_ctr(&aes->ctx, n, &aes->offset, aes->iv.data(), aes->block.data(), in, out);
} else {
rc = mbedtls_aes_crypt_cfb128(&aes->ctx, aes->mode, n, &aes->offset, aes->iv.data(), in, out);
}
} else if (auto *cam = std::get_if<Camellia>(&impl_)) {
rc = mbedtls_camellia_crypt_cfb128(&cam->ctx, cam->mode, n, &cam->offset, cam->iv.data(), in, out);
} else if (auto *s = std::get_if<Sodium>(&impl_)) {
rc = 0;
std::size_t inner = static_cast<std::size_t>(s->position % kSodiumBlock);
if (inner != 0) {
// Finish the current keystream block from a scratch copy.
std::uint8_t block[kSodiumBlock] = {};
rc = sodium_xor(s->kind, block, block, kSodiumBlock, s->nonce.data(), s->position / kSodiumBlock,
s->key.data());
std::size_t take = std::min(kSodiumBlock - inner, n);
for (std::size_t i = 0; i < take; ++i) {
out[i] = in[i] ^ block[inner + i];
}
sodium_memzero(block, sizeof(block));
in += take;
out += take;
n -= take;
s->position += take;
}
if (rc == 0 && n != 0) {
rc = sodium_xor(s->kind, out, in, n, s->nonce.data(), s->position / kSodiumBlock, s->key.data());
s->position += n;
}
}
if (rc != 0) {
return fail(Errc::crypto);
}
return {};
}
Status StreamTcpEncryptor::init(const StreamKey &key, ReplayFilter filter) noexcept
{
key_ = key;
filter_ = filter;
iv_sent_ = false;
randombytes_buf(iv_.data(), key.spec.iv_len);
return cipher_.init(key_, iv(), true);
}
Result<std::size_t> StreamTcpEncryptor::seal(std::span<const std::uint8_t> plaintext,
std::span<std::uint8_t> out) noexcept
{
if (plaintext.empty()) {
return std::size_t{ 0 };
}
if (out.size() < sealed_size(plaintext.size())) {
return fail(Errc::invalid);
}
std::size_t at = 0;
if (!iv_sent_) {
std::memcpy(out.data(), iv_.data(), key_.spec.iv_len);
at += key_.spec.iv_len;
filter_.add(iv());
iv_sent_ = true;
}
SS_TRY(cipher_.apply(plaintext.data(), out.data() + at, plaintext.size()));
return at + plaintext.size();
}
Status StreamTcpDecryptor::init(const StreamKey &key, ReplayFilter filter) noexcept
{
key_ = key;
filter_ = filter;
iv_have_ = 0;
ready_ = false;
failed_ = false;
cipher_.reset();
return {};
}
Result<StreamTcpDecryptor::Step> StreamTcpDecryptor::decrypt(std::span<const std::uint8_t> in,
std::span<std::uint8_t> out) noexcept
{
if (failed_) {
return fail(Errc::closed);
}
Step step{ 0, 0 };
if (!ready_) {
std::size_t want = key_.spec.iv_len - iv_have_;
std::size_t take = std::min(want, in.size());
std::memcpy(iv_.data() + iv_have_, in.data(), take);
iv_have_ += take;
step.consumed += take;
in = in.subspan(take);
if (iv_have_ < key_.spec.iv_len) {
return step;
}
std::span<const std::uint8_t> iv(iv_.data(), key_.spec.iv_len);
// Register the IV as soon as it is known, as the C decoder does, so
// a concurrent connection cannot replay it in the meantime.
if (filter_.check(iv)) {
failed_ = true;
return fail(Errc::crypto);
}
filter_.add(iv);
Status st = cipher_.init(key_, iv, false);
if (!st) {
failed_ = true;
return fail(st.error());
}
ready_ = true;
}
std::size_t n = std::min(in.size(), out.size());
SS_TRY(cipher_.apply(in.data(), out.data(), n));
step.consumed += n;
step.produced = n;
return step;
}
Result<std::size_t> stream_udp_seal(const StreamKey &key, std::span<const std::uint8_t> plaintext,
std::span<std::uint8_t> out, ReplayFilter filter) noexcept
{
const std::size_t iv_len = key.spec.iv_len;
if (out.size() < iv_len + plaintext.size()) {
return fail(Errc::invalid);
}
randombytes_buf(out.data(), iv_len);
std::span<const std::uint8_t> iv(out.data(), iv_len);
filter.add(iv);
StreamCipher cipher;
SS_TRY(cipher.init(key, iv, true));
SS_TRY(cipher.apply(plaintext.data(), out.data() + iv_len, plaintext.size()));
return iv_len + plaintext.size();
}
Result<std::size_t> stream_udp_open(const StreamKey &key, std::span<const std::uint8_t> packet,
std::span<std::uint8_t> out, ReplayFilter filter) noexcept
{
const std::size_t iv_len = key.spec.iv_len;
if (packet.size() <= iv_len) {
return fail(Errc::protocol);
}
std::size_t n = packet.size() - iv_len;
if (out.size() < n) {
return fail(Errc::invalid);
}
std::span<const std::uint8_t> iv = packet.first(iv_len);
if (filter.check(iv)) {
return fail(Errc::crypto);
}
StreamCipher cipher;
SS_TRY(cipher.init(key, iv, false));
SS_TRY(cipher.apply(packet.data() + iv_len, out.data(), n));
filter.add(iv);
return n;
}
} // namespace ss::crypto
+159
View File
@@ -0,0 +1,159 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef SS_CRYPTO_STREAM_HPP
#define SS_CRYPTO_STREAM_HPP
// Legacy (unauthenticated) stream ciphers as value types.
//
// Replaces stream.cpp's per-connection mbedtls_cipher_context_t (allocated by
// mbedtls_cipher_setup), its static scratch buffers and the padding copies
// it makes for the libsodium ciphers. The block-cipher contexts are held by
// value through the AES/Camellia APIs directly; the libsodium ciphers track
// their keystream position and handle partial blocks with a 64-byte stack
// block. Only built with SS_ENABLE_LEGACY.
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <span>
#include <string_view>
#include <variant>
#include <mbedtls/aes.h>
#include <mbedtls/camellia.h>
#include "core/result.hpp"
#include "crypto/aead.hpp" // ReplayFilter
namespace ss::crypto {
inline constexpr std::size_t kMaxStreamIv = 16;
inline constexpr std::size_t kMaxStreamKey = 32;
enum class StreamKind : std::uint8_t {
aes_cfb,
aes_ctr,
camellia_cfb,
salsa20,
chacha20,
chacha20_ietf,
};
struct StreamSpec {
std::string_view name;
StreamKind kind;
std::uint8_t key_len;
std::uint8_t iv_len;
};
// The stream method called `name`, if this build supports it (RC4, table and
// the ciphers Mbed TLS 3 dropped are not).
std::optional<StreamSpec> find_stream(std::string_view name) noexcept;
struct StreamKey {
StreamSpec spec;
std::array<std::uint8_t, kMaxStreamKey> bytes{};
static Result<StreamKey> from_password(const StreamSpec &spec, const char *password) noexcept;
};
// Keystream for one direction of one connection.
class StreamCipher {
public:
StreamCipher() noexcept = default;
StreamCipher(const StreamCipher &) = delete;
StreamCipher &operator=(const StreamCipher &) = delete;
~StreamCipher() { reset(); }
Status init(const StreamKey &key, std::span<const std::uint8_t> iv, bool encrypt) noexcept;
void reset() noexcept;
// XOR the next n keystream bytes; `in` and `out` may be the same buffer.
Status apply(const std::uint8_t *in, std::uint8_t *out, std::size_t n) noexcept;
private:
struct Aes {
Aes() noexcept { mbedtls_aes_init(&ctx); }
~Aes() { mbedtls_aes_free(&ctx); }
Aes(const Aes &) = delete;
Aes &operator=(const Aes &) = delete;
mbedtls_aes_context ctx;
std::array<unsigned char, 16> iv{};
std::array<unsigned char, 16> block{};
std::size_t offset = 0;
bool ctr = false;
int mode = MBEDTLS_AES_ENCRYPT;
};
struct Camellia {
Camellia() noexcept { mbedtls_camellia_init(&ctx); }
~Camellia() { mbedtls_camellia_free(&ctx); }
Camellia(const Camellia &) = delete;
Camellia &operator=(const Camellia &) = delete;
mbedtls_camellia_context ctx;
std::array<unsigned char, 16> iv{};
std::size_t offset = 0;
int mode = MBEDTLS_CAMELLIA_ENCRYPT;
};
struct Sodium {
~Sodium();
StreamKind kind = StreamKind::salsa20;
std::array<std::uint8_t, kMaxStreamKey> key{};
std::array<std::uint8_t, 12> nonce{};
std::uint64_t position = 0; // keystream bytes used so far
};
std::variant<std::monostate, Aes, Camellia, Sodium> impl_;
};
class StreamTcpEncryptor {
public:
// A fresh random IV.
Status init(const StreamKey &key, ReplayFilter filter) noexcept;
std::size_t sealed_size(std::size_t plaintext_len) const noexcept
{
return plaintext_len + (iv_sent_ ? 0 : key_.spec.iv_len);
}
// The first call also emits the IV; returns the bytes written.
Result<std::size_t> seal(std::span<const std::uint8_t> plaintext, std::span<std::uint8_t> out) noexcept;
std::span<const std::uint8_t> iv() const noexcept { return { iv_.data(), key_.spec.iv_len }; }
private:
StreamKey key_{};
ReplayFilter filter_{};
StreamCipher cipher_;
std::array<std::uint8_t, kMaxStreamIv> iv_{};
bool iv_sent_ = false;
};
class StreamTcpDecryptor {
public:
Status init(const StreamKey &key, ReplayFilter filter) noexcept;
struct Step {
std::size_t consumed;
std::size_t produced;
};
// Collect the IV, then decrypt min(remaining input, out.size()) bytes.
// Fails with Errc::crypto when the IV was seen before.
Result<Step> decrypt(std::span<const std::uint8_t> in, std::span<std::uint8_t> out) noexcept;
bool iv_received() const noexcept { return ready_; }
private:
StreamKey key_{};
ReplayFilter filter_{};
StreamCipher cipher_;
std::array<std::uint8_t, kMaxStreamIv> iv_{};
std::size_t iv_have_ = 0;
bool ready_ = false;
bool failed_ = false;
};
// One-shot UDP packet: [iv][ciphertext].
Result<std::size_t> stream_udp_seal(const StreamKey &key, std::span<const std::uint8_t> plaintext,
std::span<std::uint8_t> out, ReplayFilter filter) noexcept;
Result<std::size_t> stream_udp_open(const StreamKey &key, std::span<const std::uint8_t> packet,
std::span<std::uint8_t> out, ReplayFilter filter) noexcept;
} // namespace ss::crypto
#endif
+14
View File
@@ -219,6 +219,14 @@ ss_add_test(test_crypto_aead_cpp
"${TEST_CRYPTO_LIBS};${SS_MATH_LIBRARY}")
ss_strict_warnings(test_crypto_aead_cpp.cpp ${PROJECT_SOURCE_DIR}/src/crypto/aead.cpp)
if(SS_ENABLE_LEGACY)
# test_crypto_stream_cpp - C++ stream codecs, differentially against stream.cpp
ss_add_test(test_crypto_stream_cpp
"test_crypto_stream_cpp.cpp;${PROJECT_SOURCE_DIR}/src/crypto/stream.cpp;${PROJECT_SOURCE_DIR}/src/crypto/aead.cpp;${SS_CRYPTO_TEST_SOURCES}"
"${TEST_CRYPTO_LIBS};${SS_MATH_LIBRARY}")
ss_strict_warnings(test_crypto_stream_cpp.cpp ${PROJECT_SOURCE_DIR}/src/crypto/stream.cpp)
endif()
# test_interop_rust - interoperability against shadowsocks-rust.
# Skips itself when shadowsocks-rust or the built binaries are absent.
add_test(NAME test_interop_rust
@@ -300,6 +308,12 @@ if(SS_BUILD_EXECUTABLES AND NOT CMAKE_CROSSCOMPILING)
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/alloc_audit/check_steady.py
--shim $<TARGET_FILE:ss_alloc_audit> -- $<TARGET_FILE:test_crypto_aead_cpp>)
set_tests_properties(alloc_steady_crypto_aead PROPERTIES LABELS "unit" TIMEOUT 60)
if(TARGET test_crypto_stream_cpp)
add_test(NAME alloc_steady_crypto_stream
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/alloc_audit/check_steady.py
--shim $<TARGET_FILE:ss_alloc_audit> -- $<TARGET_FILE:test_crypto_stream_cpp>)
set_tests_properties(alloc_steady_crypto_stream PROPERTIES LABELS "unit" TIMEOUT 60)
endif()
add_test(NAME alloc_steady_cache
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/alloc_audit/check_steady.py
--shim $<TARGET_FILE:ss_alloc_audit> -- $<TARGET_FILE:test_cache_cpp>)
+255
View File
@@ -0,0 +1,255 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
// The C++ stream codecs (src/crypto/stream.hpp) differentially against the C
// implementation in stream.cpp, plus replay handling and a zero-allocation
// steady phase.
#include <cstdint>
#include <cstring>
#include <vector>
#include <sodium.h>
#include "alloc_audit/alloc_probe.hpp"
#include "crypto.h"
#include "crypto/stream.hpp"
#include "ppbloom.h"
#include "test_helpers.hpp"
#include "utils.h"
#undef min
#undef max
extern "C" {
int verbose = 0;
struct cache *nonce_cache = nullptr;
}
namespace sc = ss::crypto;
namespace {
std::vector<std::uint8_t> random_bytes(std::size_t n)
{
std::vector<std::uint8_t> v(n);
randombytes_buf(v.data(), n);
return v;
}
struct CMethod {
crypto_t *crypto = nullptr;
explicit CMethod(const char *method)
{
crypto = crypto_init("password", nullptr, method);
SS_CHECK(crypto != nullptr);
}
~CMethod()
{
ppbloom_free();
ss_free(crypto->cipher);
ss_free(crypto);
}
};
// Odd sizes so that writes start and end inside 64-byte keystream blocks.
std::size_t piece(std::size_t left)
{
return std::min<std::size_t>(1 + randombytes_uniform(3000), left);
}
void tcp_c_to_cpp(const char *method, const sc::StreamKey &key)
{
CMethod c(method);
cipher_ctx_t enc;
c.crypto->ctx_init(c.crypto->cipher, &enc, 1);
auto plaintext = random_bytes(60000);
std::vector<std::uint8_t> wire;
buffer_t buf{};
const std::size_t cap = 8192;
SS_CHECK(balloc(&buf, cap) > 0);
for (std::size_t off = 0; off < plaintext.size();) {
std::size_t n = piece(plaintext.size() - off);
std::memcpy(buf.data, plaintext.data() + off, n);
buf.len = n;
SS_CHECK(c.crypto->encrypt(&buf, &enc, cap) == CRYPTO_OK);
wire.insert(wire.end(), buf.data, buf.data + buf.len);
off += n;
}
bfree(&buf);
c.crypto->ctx_release(&enc);
sc::StreamTcpDecryptor dec;
SS_CHECK(dec.init(key, {}));
std::vector<std::uint8_t> got;
std::vector<std::uint8_t> out(1000); // smaller than some inputs on purpose
for (std::size_t off = 0; off < wire.size();) {
std::size_t n = std::min<std::size_t>(1 + randombytes_uniform(5000), wire.size() - off);
std::span<const std::uint8_t> in(wire.data() + off, n);
while (!in.empty()) {
auto step = dec.decrypt(in, out);
SS_CHECK(step);
got.insert(got.end(), out.begin(), out.begin() + step->produced);
in = in.subspan(step->consumed);
}
off += n;
}
SS_CHECK(got == plaintext);
}
void tcp_cpp_to_c(const char *method, const sc::StreamKey &key)
{
CMethod c(method);
cipher_ctx_t dec;
c.crypto->ctx_init(c.crypto->cipher, &dec, 0);
sc::StreamTcpEncryptor enc;
SS_CHECK(enc.init(key, {}));
auto plaintext = random_bytes(60000);
std::vector<std::uint8_t> got;
buffer_t buf{};
const std::size_t cap = 8192;
SS_CHECK(balloc(&buf, cap) > 0);
for (std::size_t off = 0; off < plaintext.size();) {
std::size_t n = piece(plaintext.size() - off);
std::vector<std::uint8_t> wire(enc.sealed_size(n));
auto w = enc.seal({ plaintext.data() + off, n }, wire);
SS_CHECK(w && *w == wire.size());
std::memcpy(buf.data, wire.data(), wire.size());
buf.len = wire.size();
int rc = c.crypto->decrypt(&buf, &dec, cap);
SS_CHECK(rc == CRYPTO_OK || (rc == CRYPTO_NEED_MORE && buf.len == 0));
got.insert(got.end(), buf.data, buf.data + buf.len);
off += n;
}
bfree(&buf);
c.crypto->ctx_release(&dec);
SS_CHECK(got == plaintext);
}
void udp_both_ways(const char *method, const sc::StreamKey &key)
{
CMethod c(method);
const std::size_t cap = 65536;
for (std::size_t size : { 1u, 63u, 64u, 65u, 1400u }) {
auto payload = random_bytes(size);
buffer_t buf{};
SS_CHECK(balloc(&buf, cap) > 0);
std::memcpy(buf.data, payload.data(), size);
buf.len = size;
SS_CHECK(c.crypto->encrypt_all(&buf, c.crypto->cipher, cap) == CRYPTO_OK);
std::vector<std::uint8_t> out(size);
auto n = sc::stream_udp_open(key, { reinterpret_cast<std::uint8_t *>(buf.data), buf.len }, out, {});
SS_CHECK(n && *n == size && std::memcmp(out.data(), payload.data(), size) == 0);
std::vector<std::uint8_t> packet(key.spec.iv_len + size);
auto w = sc::stream_udp_seal(key, payload, packet, {});
SS_CHECK(w && *w == packet.size());
std::memcpy(buf.data, packet.data(), packet.size());
buf.len = packet.size();
SS_CHECK(c.crypto->decrypt_all(&buf, c.crypto->cipher, cap) == CRYPTO_OK);
SS_CHECK(buf.len == size && std::memcmp(buf.data, payload.data(), size) == 0);
bfree(&buf);
}
}
struct IvSet {
std::vector<std::vector<std::uint8_t> > ivs;
static bool seen(void *ctx, std::span<const std::uint8_t> iv) noexcept
{
for (auto &s : static_cast<IvSet *>(ctx)->ivs) {
if (s.size() == iv.size() && std::memcmp(s.data(), iv.data(), iv.size()) == 0) {
return true;
}
}
return false;
}
static void remember(void *ctx, std::span<const std::uint8_t> iv) noexcept
{
static_cast<IvSet *>(ctx)->ivs.emplace_back(iv.begin(), iv.end());
}
sc::ReplayFilter filter() { return { &seen, &remember, this }; }
};
void replay(const sc::StreamKey &key)
{
sc::StreamTcpEncryptor enc;
SS_CHECK(enc.init(key, {}));
std::uint8_t msg[10] = {};
std::vector<std::uint8_t> wire(enc.sealed_size(sizeof(msg)));
SS_CHECK(enc.seal(msg, wire));
IvSet seen;
std::uint8_t out[16];
sc::StreamTcpDecryptor first, second;
SS_CHECK(first.init(key, seen.filter()) && second.init(key, seen.filter()));
// The IV alone is enough to register it.
auto step = first.decrypt(std::span<const std::uint8_t>(wire.data(), key.spec.iv_len), out);
SS_CHECK(step && step->produced == 0 && first.iv_received());
auto again = second.decrypt(wire, out);
SS_CHECK(!again && again.error().code == ss::Errc::crypto);
}
void steady(const sc::StreamKey &key)
{
static std::uint8_t plain[4096], wire[4096 + 16], out[4096];
for (int conn = 0; conn < 100; ++conn) {
sc::StreamTcpEncryptor enc;
sc::StreamTcpDecryptor dec;
SS_CHECK(enc.init(key, {}) && dec.init(key, {}));
for (std::size_t n : { (std::size_t)1, (std::size_t)100, (std::size_t)4000 }) {
auto w = enc.seal({ plain, n }, wire);
SS_CHECK(w);
auto step = dec.decrypt({ wire, *w }, out);
SS_CHECK(step && step->produced == n);
}
auto p = sc::stream_udp_seal(key, { plain, 1200 }, wire, {});
SS_CHECK(p && sc::stream_udp_open(key, { wire, *p }, out, {}));
}
}
const char *const kMethods[] = { "aes-128-cfb", "aes-192-cfb", "aes-256-cfb", "aes-128-ctr", "aes-192-ctr",
"aes-256-ctr", "camellia-128-cfb", "camellia-192-cfb", "camellia-256-cfb",
"salsa20", "chacha20", "chacha20-ietf" };
sc::StreamKey key_for(const char *name)
{
auto spec = sc::find_stream(name);
SS_CHECK(spec.has_value());
auto key = sc::StreamKey::from_password(spec.value(), "password");
SS_CHECK(key);
return *key;
}
void test_methods()
{
for (const char *m : kMethods) {
sc::StreamKey key = key_for(m);
tcp_c_to_cpp(m, key);
tcp_cpp_to_c(m, key);
udp_both_ways(m, key);
replay(key);
std::printf("ok %s\n", m);
}
SS_CHECK(!sc::find_stream("rc4-md5") && !sc::find_stream("aes-256-gcm"));
}
void test_no_heap_in_steady_state()
{
sc::StreamKey keys[sizeof(kMethods) / sizeof(kMethods[0])];
std::size_t i = 0;
for (const char *m : kMethods) {
keys[i] = key_for(m);
steady(keys[i++]);
}
ss::test::alloc_mark();
for (const sc::StreamKey &key : keys) {
steady(key);
}
ss::test::alloc_mark();
}
} // namespace
int main()
{
SS_CHECK(sodium_init() != -1);
SS_RUN(test_methods);
SS_RUN(test_no_heap_in_steady_state);
return 0;
}