C++20 port 10/14: allocation-free SIP022 codecs (#3074)

* Add allocation-free C++ SIP022 codecs

TCP request/response streams with the salt echo between the two
directions, an exact-match salt pool on FixedLru, and UDP packets sealed
and opened in place with caller-owned session state. Differentially
tested against aead2022.cpp in both roles; the steady phase allocates
nothing.

* Include <algorithm> for std::min in test_crypto_aead2022_cpp.cpp

* Include C++ headers before crypto.h in test_crypto_aead2022_cpp.cpp

crypto.h defines min/max macros for the C sources; libstdc++ headers
included after it fail to parse.
This commit is contained in:
Max Lv
2026-10-15 10:04:04 +08:00
committed by GitHub
parent 7041693ee4
commit 5dbcbcbb89
5 changed files with 1320 additions and 0 deletions
+8
View File
@@ -239,6 +239,14 @@ using Status = Result<void>;
} \
} while (0)
// Declare `name` as the value of a Result, or propagate its error.
#define SS_TRY_ASSIGN(name, expr) \
auto name##_result_ = (expr); \
if (!name##_result_) { \
return ::ss::fail(name##_result_.error()); \
} \
auto name = std::move(*name##_result_)
} // namespace ss
#endif
+682
View File
@@ -0,0 +1,682 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#include "crypto/aead2022.hpp"
#include <algorithm>
#include <cstring>
#include <ctime>
#include <mbedtls/aes.h>
#include <sodium.h>
#include "base64.h"
#include "blake3/blake3.h"
namespace ss::crypto {
namespace {
constexpr Aead2022Spec kSpecs[] = {
{ "2022-blake3-aes-128-gcm", { "2022-blake3-aes-128-gcm", AeadKind::aes_128_gcm, 16, 12, 16 } },
{ "2022-blake3-aes-256-gcm", { "2022-blake3-aes-256-gcm", AeadKind::aes_256_gcm, 32, 12, 16 } },
{ "2022-blake3-chacha20-poly1305",
{ "2022-blake3-chacha20-poly1305", AeadKind::chacha20_poly1305, 32, 12, 16 } },
};
constexpr char kSubkeyContext[] = "shadowsocks 2022 session subkey";
constexpr std::uint8_t kTypeClient = 0;
constexpr std::uint8_t kTypeServer = 1;
constexpr std::size_t kLenSize = 2;
constexpr std::size_t kReqFixed = 1 + 8 + 2; // type, timestamp, length
constexpr std::size_t kUdpMinHdr = 1 + 8 + 2; // type, timestamp, padding length
constexpr std::size_t kMaxFixed = kReqFixed + kMaxKey; // response: + request salt
std::size_t resp_fixed(std::size_t salt_len) noexcept
{
return kReqFixed + salt_len;
}
void store16_be(std::uint8_t *p, std::uint16_t v) noexcept
{
p[0] = static_cast<std::uint8_t>(v >> 8);
p[1] = static_cast<std::uint8_t>(v);
}
std::uint16_t load16_be(const std::uint8_t *p) noexcept
{
return static_cast<std::uint16_t>((p[0] << 8) | p[1]);
}
void store64_be(std::uint8_t *p, std::uint64_t v) noexcept
{
for (int i = 7; i >= 0; --i) {
p[i] = static_cast<std::uint8_t>(v & 0xff);
v >>= 8;
}
}
std::uint64_t load64_be(const std::uint8_t *p) noexcept
{
std::uint64_t v = 0;
for (int i = 0; i < 8; ++i) {
v = (v << 8) | p[i];
}
return v;
}
std::uint64_t unix_now() noexcept
{
return static_cast<std::uint64_t>(std::time(nullptr));
}
bool timestamp_ok(std::uint64_t ts, std::uint64_t now) noexcept
{
std::uint64_t diff = now > ts ? now - ts : ts - now;
return diff <= k2022TimeSkew;
}
// Length of the SOCKS address (type, address, port) at the head of `buf`, or
// 0 if it is malformed or truncated.
std::size_t socks_addr_len(std::span<const std::uint8_t> buf) noexcept
{
if (buf.empty()) {
return 0;
}
std::size_t len = 0;
switch (buf[0] & 0x0F) {
case 1:
len = 1 + 4;
break;
case 4:
len = 1 + 16;
break;
case 3:
if (buf.size() < 2) {
return 0;
}
len = 2 + buf[1];
break;
default:
return 0;
}
return buf.size() >= len + 2 ? len + 2 : 0;
}
std::size_t chunks_cost(std::size_t n) noexcept
{
std::size_t chunks = (n + k2022MaxChunk - 1) / k2022MaxChunk;
return n + chunks * (kLenSize + 2 * kTag);
}
// AES-ECB over the 16-byte separate header, keyed with the PSK.
Status sep_header(const Psk &psk, std::uint8_t *dst, const std::uint8_t *src, bool encrypt) noexcept
{
mbedtls_aes_context aes;
mbedtls_aes_init(&aes);
unsigned bits = static_cast<unsigned>(psk.size() * 8);
int rc = encrypt ? mbedtls_aes_setkey_enc(&aes, psk.bytes.data(), bits)
: mbedtls_aes_setkey_dec(&aes, psk.bytes.data(), bits);
if (rc == 0) {
rc = mbedtls_aes_crypt_ecb(&aes, encrypt ? MBEDTLS_AES_ENCRYPT : MBEDTLS_AES_DECRYPT, src, dst);
}
mbedtls_aes_free(&aes);
if (rc != 0) {
return fail(Errc::crypto);
}
return {};
}
void window_reset(UdpSession2022 &s, const std::array<std::uint8_t, kSessionIdLen> &id, std::uint64_t packet) noexcept
{
s.rx_id = id;
s.rx_window.fill(0);
// Packet 0 is legitimate for a fresh session.
s.rx_highest = packet > 0 ? packet - 1 : 0;
s.rx_ready = true;
}
bool window_check(const UdpSession2022 &s, std::uint64_t id) noexcept
{
if (id > s.rx_highest) {
return true;
}
if (s.rx_highest - id >= kUdpWindowBits) {
return false;
}
std::uint64_t bit = id % kUdpWindowBits;
return (s.rx_window[bit / 8] & (1U << (bit % 8))) == 0;
}
void window_update(UdpSession2022 &s, std::uint64_t id) noexcept
{
if (id > s.rx_highest) {
std::uint64_t shift = id - s.rx_highest;
if (shift >= kUdpWindowBits) {
s.rx_window.fill(0);
} else {
for (std::uint64_t i = s.rx_highest + 1; i <= id; ++i) {
std::uint64_t b = i % kUdpWindowBits;
s.rx_window[b / 8] &= static_cast<std::uint8_t>(~(1U << (b % 8)));
}
}
s.rx_highest = id;
}
std::uint64_t bit = id % kUdpWindowBits;
s.rx_window[bit / 8] |= static_cast<std::uint8_t>(1U << (bit % 8));
}
std::size_t udp_header_len(Role sender) noexcept
{
return kUdpMinHdr + (sender == Role::server ? kSessionIdLen : 0);
}
} // namespace
std::optional<Aead2022Spec> find_aead2022(std::string_view name) noexcept
{
for (const Aead2022Spec &spec : kSpecs) {
if (spec.name == name) {
return spec;
}
}
return std::nullopt;
}
Result<Psk> Psk::from_base64(const Aead2022Spec &spec, const char *key) noexcept
{
if (key == nullptr) {
return fail(Errc::config);
}
Psk psk{ spec, {} };
// One spare byte tells an over-long key from an exact one.
std::uint8_t out[kMaxKey + 1];
int n = base64_decode(out, key, static_cast<int>(spec.aead.key_len) + 1);
if (n != static_cast<int>(spec.aead.key_len)) {
sodium_memzero(out, sizeof(out));
return fail(Errc::config);
}
std::memcpy(psk.bytes.data(), out, spec.aead.key_len);
sodium_memzero(out, sizeof(out));
return psk;
}
void derive_subkey_2022(std::span<const std::uint8_t> psk, std::span<const std::uint8_t> salt,
std::span<std::uint8_t> subkey) noexcept
{
blake3_hasher hasher;
blake3_hasher_init_derive_key(&hasher, kSubkeyContext);
blake3_hasher_update(&hasher, psk.data(), psk.size());
blake3_hasher_update(&hasher, salt.data(), salt.size());
blake3_hasher_finalize(&hasher, subkey.data(), subkey.size());
sodium_memzero(&hasher, sizeof(hasher));
}
bool SaltPool::check_and_add(std::span<const std::uint8_t> salt, std::uint64_t now) noexcept
{
auto key = FixedKey<kMaxKey>::from(std::as_bytes(salt));
if (!key) {
return false;
}
// Forget salts older than the retention period; they are oldest first.
while (const std::uint64_t *first = seen_.oldest()) {
if (*first + k2022SaltTtl > now) {
break;
}
(void)seen_.pop_oldest();
}
if (seen_.peek(*key) != nullptr) {
return false;
}
if (seen_.full()) {
(void)seen_.pop_oldest();
}
return seen_.emplace(*key, now).has_value();
}
Status Aead2022TcpEncryptor::init(const Psk &psk, Role role, const Aead2022TcpDecryptor *request) noexcept
{
if (role == Role::server && request == nullptr) {
return fail(Errc::invalid);
}
psk_ = psk;
role_ = role;
peer_ = request;
header_sent_ = false;
nonce_.fill(0);
randombytes_buf(salt_.data(), psk.size());
std::array<std::uint8_t, kMaxKey> subkey{};
derive_subkey_2022(psk.key(), salt(), { subkey.data(), psk.size() });
Status st = engine_.init(psk.spec.aead, { subkey.data(), psk.size() });
sodium_memzero(subkey.data(), subkey.size());
return st;
}
std::size_t Aead2022TcpEncryptor::sealed_size(std::size_t plaintext_len) const noexcept
{
if (header_sent_) {
return chunks_cost(plaintext_len);
}
// salt, fixed header and its tag, the header/first chunk tag, padding.
return psk_.size() + kMaxFixed + kTag + kLenSize + k2022MaxPad + kTag + chunks_cost(plaintext_len);
}
Result<std::size_t> Aead2022TcpEncryptor::seal_one(std::span<const std::uint8_t> plaintext, std::uint8_t *out) noexcept
{
SS_TRY(engine_.seal(out, plaintext, nonce_.data()));
sodium_increment(nonce_.data(), psk_.spec.aead.nonce_len);
return plaintext.size() + kTag;
}
Result<std::size_t> Aead2022TcpEncryptor::seal_chunks(std::span<const std::uint8_t> plaintext, std::uint8_t *out) noexcept
{
std::size_t at = 0;
while (!plaintext.empty()) {
std::size_t n = std::min(plaintext.size(), k2022MaxChunk);
std::uint8_t len[kLenSize];
store16_be(len, static_cast<std::uint16_t>(n));
SS_TRY_ASSIGN(w, seal_one(len, out + at));
at += w;
SS_TRY_ASSIGN(p, seal_one(plaintext.first(n), out + at));
at += p;
plaintext = plaintext.subspan(n);
}
return at;
}
Result<std::size_t> Aead2022TcpEncryptor::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);
}
if (header_sent_) {
return seal_chunks(plaintext, out.data());
}
const std::size_t salt_len = psk_.size();
std::uint8_t fixed[kMaxFixed];
std::size_t fixed_len = 0;
std::size_t at = salt_len;
std::memcpy(out.data(), salt_.data(), salt_len);
std::span<const std::uint8_t> rest;
if (role_ == Role::client) {
std::size_t addr_len = socks_addr_len(plaintext);
if (addr_len == 0) {
return fail(Errc::protocol);
}
auto payload = plaintext.subspan(addr_len);
std::size_t now = std::min(payload.size(), k2022MaxChunk - addr_len - kLenSize);
std::uint16_t pad = 0;
if (payload.empty()) {
// SIP022 requires padding when no payload follows the address.
pad = static_cast<std::uint16_t>(randombytes_uniform(k2022MaxPad) + 1);
}
std::size_t body_len = addr_len + kLenSize + pad + now;
fixed[0] = kTypeClient;
store64_be(fixed + 1, unix_now());
store16_be(fixed + 9, static_cast<std::uint16_t>(body_len));
fixed_len = kReqFixed;
SS_TRY_ASSIGN(f, seal_one({ fixed, fixed_len }, out.data() + at));
at += f;
// Assemble the header body in place and encrypt it there.
std::uint8_t *body = out.data() + at;
std::memcpy(body, plaintext.data(), addr_len);
store16_be(body + addr_len, pad);
if (pad != 0) {
randombytes_buf(body + addr_len + kLenSize, pad);
}
if (now != 0) {
std::memcpy(body + addr_len + kLenSize + pad, payload.data(), now);
}
SS_TRY_ASSIGN(b, seal_one({ body, body_len }, body));
at += b;
rest = payload.subspan(now);
} else {
if (peer_ == nullptr || !peer_->header_received()) {
return fail(Errc::protocol); // nothing to answer yet
}
std::size_t now = std::min(plaintext.size(), k2022MaxChunk);
fixed[0] = kTypeServer;
store64_be(fixed + 1, unix_now());
std::memcpy(fixed + 9, peer_->salt().data(), salt_len);
store16_be(fixed + 9 + salt_len, static_cast<std::uint16_t>(now));
fixed_len = resp_fixed(salt_len);
SS_TRY_ASSIGN(f, seal_one({ fixed, fixed_len }, out.data() + at));
at += f;
SS_TRY_ASSIGN(b, seal_one(plaintext.first(now), out.data() + at));
at += b;
rest = plaintext.subspan(now);
}
header_sent_ = true;
SS_TRY_ASSIGN(tail, seal_chunks(rest, out.data() + at));
return at + tail;
}
Status Aead2022TcpDecryptor::init(const Psk &psk, Role role, std::span<const std::uint8_t> request_salt,
SaltPool *pool) noexcept
{
if (role == Role::client && request_salt.size() != psk.size()) {
return fail(Errc::invalid);
}
psk_ = psk;
role_ = role;
pool_ = pool;
stage_ = Stage::fixed;
failed_ = false;
want_ = 0;
nonce_.fill(0);
request_salt_.fill(0);
if (!request_salt.empty()) {
std::memcpy(request_salt_.data(), request_salt.data(), request_salt.size());
}
buffer_.clear();
engine_.reset();
return {};
}
Result<Aead2022TcpDecryptor::Step> Aead2022TcpDecryptor::decrypt(std::span<const std::uint8_t> in,
std::span<std::uint8_t> out) noexcept
{
if (failed_) {
return fail(Errc::closed);
}
if (out.size() < k2022MaxChunk) {
return fail(Errc::invalid);
}
Step step{ 0, 0 };
if (buffer_.writable().size() < in.size()) {
buffer_.compact();
}
std::size_t take = std::min(in.size(), buffer_.writable().size());
if (take != 0) {
std::memcpy(buffer_.writable().data(), in.data(), take);
(void)buffer_.commit(take);
step.consumed = take;
}
auto bytes = [this]() {
auto r = buffer_.readable();
return std::span<const std::uint8_t>(reinterpret_cast<const std::uint8_t *>(r.data()), r.size());
};
auto broken = [this](Errc code) {
failed_ = true;
return fail(code);
};
auto open = [this](std::uint8_t *dst, std::span<const std::uint8_t> chunk) {
bool ok = static_cast<bool>(engine_.open(dst, chunk, nonce_.data()));
if (ok) {
sodium_increment(nonce_.data(), psk_.spec.aead.nonce_len);
}
return ok;
};
const std::size_t salt_len = psk_.size();
for (;;) {
auto data = bytes();
switch (stage_) {
case Stage::fixed: {
// A server reads requests; a client reads responses.
std::size_t fixed_len = role_ == Role::server ? kReqFixed : resp_fixed(salt_len);
if (data.size() < salt_len + fixed_len + kTag) {
return step;
}
std::memcpy(salt_.data(), data.data(), salt_len);
std::array<std::uint8_t, kMaxKey> subkey{};
derive_subkey_2022(psk_.key(), salt(), { subkey.data(), salt_len });
Status st = engine_.init(psk_.spec.aead, { subkey.data(), salt_len });
sodium_memzero(subkey.data(), subkey.size());
if (!st) {
return broken(st.error().code);
}
std::uint8_t fixed[kMaxFixed];
if (!open(fixed, data.subspan(salt_len, fixed_len + kTag))) {
return broken(Errc::crypto);
}
std::uint64_t now = unix_now();
if (fixed[0] != (role_ == Role::server ? kTypeClient : kTypeServer)
|| !timestamp_ok(load64_be(fixed + 1), now)) {
return broken(Errc::protocol);
}
if (role_ == Role::client) {
if (sodium_memcmp(fixed + 9, request_salt_.data(), salt_len) != 0) {
return broken(Errc::crypto);
}
want_ = load16_be(fixed + 9 + salt_len);
} else {
// Only authentic salts enter the pool.
if (pool_ != nullptr && !pool_->check_and_add(salt(), now)) {
return broken(Errc::crypto);
}
want_ = load16_be(fixed + 9);
}
if (want_ == 0) {
return broken(Errc::protocol);
}
(void)buffer_.consume(salt_len + fixed_len + kTag);
stage_ = Stage::variable;
break;
}
case Stage::variable: {
if (data.size() < want_ + kTag || out.size() - step.produced < want_) {
return step;
}
std::uint8_t *dst = out.data() + step.produced;
if (!open(dst, data.first(want_ + kTag))) {
return broken(Errc::crypto);
}
(void)buffer_.consume(want_ + kTag);
if (role_ == Role::client) {
step.produced += want_; // the first response payload
} else {
std::span<const std::uint8_t> hdr(dst, want_);
std::size_t addr_len = socks_addr_len(hdr);
if (addr_len == 0 || want_ < addr_len + kLenSize) {
return broken(Errc::protocol);
}
std::size_t pad = load16_be(dst + addr_len);
std::size_t off = addr_len + kLenSize + pad;
if (pad > k2022MaxPad || off > want_) {
return broken(Errc::protocol);
}
std::size_t payload = want_ - off;
if (payload == 0 && pad == 0) {
return broken(Errc::protocol); // SIP022: neither payload nor padding
}
std::memmove(dst + addr_len, dst + off, payload);
step.produced += addr_len + payload;
}
stage_ = Stage::length;
break;
}
case Stage::length: {
if (data.size() < kLenSize + kTag) {
return step;
}
std::uint8_t len[kLenSize];
if (!open(len, data.first(kLenSize + kTag))) {
return broken(Errc::crypto);
}
want_ = load16_be(len);
if (want_ == 0) {
return broken(Errc::protocol);
}
(void)buffer_.consume(kLenSize + kTag);
stage_ = Stage::payload;
break;
}
case Stage::payload: {
if (data.size() < want_ + kTag || out.size() - step.produced < want_) {
return step;
}
if (!open(out.data() + step.produced, data.first(want_ + kTag))) {
return broken(Errc::crypto);
}
(void)buffer_.consume(want_ + kTag);
step.produced += want_;
stage_ = Stage::length;
break;
}
}
}
}
UdpSession2022::~UdpSession2022()
{
sodium_memzero(tx_subkey.data(), tx_subkey.size());
}
std::size_t udp2022_overhead(const Psk &psk, Role role) noexcept
{
std::size_t prefix = psk.chacha() ? kUdpXNonce + kUdpSepHeader : kUdpSepHeader;
return prefix + udp_header_len(role) + kTag;
}
Result<std::size_t> udp2022_seal(const Psk &psk, Role role, UdpSession2022 &session,
std::span<const std::uint8_t> plaintext, std::span<std::uint8_t> out) noexcept
{
const std::size_t total = udp2022_overhead(psk, role) + plaintext.size();
if (out.size() < total) {
return fail(Errc::invalid);
}
if (role == Role::server && !session.peer_known) {
return fail(Errc::protocol); // no client session to reply to
}
if (!session.tx_ready) {
randombytes_buf(session.tx_id.data(), kSessionIdLen);
session.tx_packet = 0;
if (!psk.chacha()) {
derive_subkey_2022(psk.key(), session.tx_id, { session.tx_subkey.data(), psk.size() });
}
session.tx_ready = true;
}
std::uint8_t sep[kUdpSepHeader];
std::memcpy(sep, session.tx_id.data(), kSessionIdLen);
store64_be(sep + kSessionIdLen, session.tx_packet);
// The body (main header + payload) is assembled where its ciphertext
// goes and encrypted in place; for ChaCha the separate header is part
// of the encrypted body.
std::uint8_t *body = out.data() + (psk.chacha() ? kUdpXNonce + kUdpSepHeader : kUdpSepHeader);
std::size_t o = 0;
body[o++] = role == Role::server ? kTypeServer : kTypeClient;
store64_be(body + o, unix_now());
o += 8;
if (role == Role::server) {
std::memcpy(body + o, session.peer_id.data(), kSessionIdLen);
o += kSessionIdLen;
}
store16_be(body + o, 0); // no padding
o += kLenSize;
std::memmove(body + o, plaintext.data(), plaintext.size());
std::size_t body_len = o + plaintext.size();
if (psk.chacha()) {
std::uint8_t *nonce = out.data();
randombytes_buf(nonce, kUdpXNonce);
std::uint8_t *merged = nonce + kUdpXNonce;
std::memcpy(merged, sep, kUdpSepHeader);
unsigned long long clen = 0;
if (crypto_aead_xchacha20poly1305_ietf_encrypt(merged, &clen, merged, kUdpSepHeader + body_len, nullptr, 0,
nullptr, nonce, psk.bytes.data())
!= 0) {
return fail(Errc::crypto);
}
} else {
SS_TRY(sep_header(psk, out.data(), sep, true));
AeadEngine engine;
SS_TRY(engine.init(psk.spec.aead, { session.tx_subkey.data(), psk.size() }));
SS_TRY(engine.seal(body, { body, body_len }, sep + 4));
}
++session.tx_packet;
return total;
}
Result<Udp2022Packet> udp2022_decrypt(const Psk &psk, std::span<const std::uint8_t> packet,
std::span<std::uint8_t> out) noexcept
{
if (out.size() < packet.size()) {
return fail(Errc::invalid);
}
Udp2022Packet result{};
if (psk.chacha()) {
if (packet.size() <= kUdpXNonce + kUdpSepHeader + kTag) {
return fail(Errc::protocol);
}
unsigned long long plen = 0;
if (crypto_aead_xchacha20poly1305_ietf_decrypt(out.data(), &plen, nullptr, packet.data() + kUdpXNonce,
packet.size() - kUdpXNonce, nullptr, 0, packet.data(),
psk.bytes.data())
!= 0) {
return fail(Errc::crypto);
}
std::memcpy(result.session_id.data(), out.data(), kSessionIdLen);
result.packet_id = load64_be(out.data() + kSessionIdLen);
result.body_len = static_cast<std::size_t>(plen) - kUdpSepHeader;
std::memmove(out.data(), out.data() + kUdpSepHeader, result.body_len);
return result;
}
if (packet.size() <= kUdpSepHeader + kTag) {
return fail(Errc::protocol);
}
std::uint8_t sep[kUdpSepHeader];
SS_TRY(sep_header(psk, sep, packet.data(), false));
std::memcpy(result.session_id.data(), sep, kSessionIdLen);
result.packet_id = load64_be(sep + kSessionIdLen);
std::array<std::uint8_t, kMaxKey> subkey{};
derive_subkey_2022(psk.key(), result.session_id, { subkey.data(), psk.size() });
AeadEngine engine;
Status st = engine.init(psk.spec.aead, { subkey.data(), psk.size() });
sodium_memzero(subkey.data(), subkey.size());
SS_TRY(st);
SS_TRY(engine.open(out.data(), packet.subspan(kUdpSepHeader), sep + 4));
result.body_len = packet.size() - kUdpSepHeader - kTag;
return result;
}
Result<std::size_t> udp2022_accept(Role role, UdpSession2022 &session, const Udp2022Packet &packet,
std::span<std::uint8_t> out, std::uint64_t now) noexcept
{
// We receive what the other role sent.
const Role sender = role == Role::server ? Role::client : Role::server;
const std::size_t n = packet.body_len;
if (n < udp_header_len(sender) || out.size() < n) {
return fail(Errc::protocol);
}
const std::uint8_t *body = out.data();
if (body[0] != (sender == Role::server ? kTypeServer : kTypeClient) || !timestamp_ok(load64_be(body + 1), now)) {
return fail(Errc::protocol);
}
std::size_t o = 9;
if (role == Role::client) {
// The server echoes our session ID.
if (!session.tx_ready || std::memcmp(body + o, session.tx_id.data(), kSessionIdLen) != 0) {
return fail(Errc::protocol);
}
o += kSessionIdLen;
}
std::size_t pad = load16_be(body + o);
o += kLenSize;
if (o + pad > n) {
return fail(Errc::protocol);
}
o += pad;
if (!session.rx_ready || session.rx_id != packet.session_id) {
window_reset(session, packet.session_id, packet.packet_id);
} else if (!window_check(session, packet.packet_id)) {
return fail(Errc::crypto); // replayed
}
window_update(session, packet.packet_id);
if (role == Role::server) {
session.peer_id = packet.session_id;
session.peer_known = true;
}
std::memmove(out.data(), out.data() + o, n - o);
return n - o;
}
} // namespace ss::crypto
+194
View File
@@ -0,0 +1,194 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
#ifndef SS_CRYPTO_AEAD2022_HPP
#define SS_CRYPTO_AEAD2022_HPP
// Shadowsocks 2022 (SIP022) as value types.
//
// Replaces aead2022.cpp / aead2022_udp.cpp, which allocate a temporary
// header body per stream, a chunk buffer per connection, an Mbed TLS cipher
// context per UDP packet and a session object per UDP conversation. Here
// the stream codecs hold one fixed reassembly buffer, a UDP packet is
// sealed or opened entirely on the stack, and UDP sessions are plain values
// whose storage the caller provides (a pool slot in the relay).
//
// SIP022 is asymmetric: a client encrypts requests and decrypts responses,
// a server does the opposite, and the response header echoes the request
// salt. The two directions of one connection are therefore linked: the
// server's encryptor reads the salt its decryptor received, and the client's
// decryptor checks the echo against the salt its encryptor sent.
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <span>
#include <string_view>
#include "core/buffer.hpp"
#include "core/fixed_map.hpp"
#include "core/result.hpp"
#include "crypto/aead.hpp"
namespace ss::crypto {
enum class Role : std::uint8_t { client, server };
inline constexpr std::size_t k2022MaxChunk = 0xFFFF; // payload bytes per TCP chunk
inline constexpr std::size_t k2022MaxPad = 900;
inline constexpr std::size_t k2022TimeSkew = 30; // seconds either way
inline constexpr std::size_t k2022SaltTtl = 60; // seconds a TCP salt is remembered
inline constexpr std::size_t kSessionIdLen = 8;
inline constexpr std::size_t kUdpSepHeader = 16; // session ID + packet ID
inline constexpr std::size_t kUdpXNonce = 24;
inline constexpr std::size_t kUdpWindowBits = 256;
struct Aead2022Spec {
std::string_view name;
AeadSpec aead; // the TCP / AES-UDP body cipher
};
std::optional<Aead2022Spec> find_aead2022(std::string_view name) noexcept;
// The pre-shared key. SIP022 forbids password derivation.
struct Psk {
Aead2022Spec spec;
std::array<std::uint8_t, kMaxKey> bytes{};
std::size_t size() const noexcept { return spec.aead.key_len; }
std::span<const std::uint8_t> key() const noexcept { return { bytes.data(), size() }; }
bool chacha() const noexcept { return spec.aead.kind == AeadKind::chacha20_poly1305; }
static Result<Psk> from_base64(const Aead2022Spec &spec, const char *key) noexcept;
};
// blake3::derive_key("shadowsocks 2022 session subkey", psk || salt).
void derive_subkey_2022(std::span<const std::uint8_t> psk, std::span<const std::uint8_t> salt,
std::span<std::uint8_t> subkey) noexcept;
// Exact-match TCP salt replay protection for servers (SIP022 forbids bloom
// filters): every salt seen in the last k2022SaltTtl seconds.
class SaltPool {
public:
Status reserve(std::uint32_t capacity, std::uint64_t seed) noexcept { return seen_.reserve(capacity, seed); }
// False if the salt was seen within the retention period; otherwise
// remembers it.
bool check_and_add(std::span<const std::uint8_t> salt, std::uint64_t now) noexcept;
std::uint32_t size() const noexcept { return seen_.size(); }
private:
FixedLru<FixedKey<kMaxKey>, std::uint64_t> seen_; // salt -> time first seen
};
class Aead2022TcpDecryptor;
class Aead2022TcpEncryptor {
public:
// A server encryptor answers the request its `request` decryptor read.
Status init(const Psk &psk, Role role, const Aead2022TcpDecryptor *request = nullptr) noexcept;
// Upper bound of the bytes seal() writes for `plaintext_len` input.
std::size_t sealed_size(std::size_t plaintext_len) const noexcept;
// Encrypt into `out` (at least sealed_size() bytes). A client's first
// plaintext must start with the SOCKS target address; it becomes the
// request header, padded when no payload follows. A server's first call
// writes the response header. 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> salt() const noexcept { return { salt_.data(), psk_.size() }; }
private:
Result<std::size_t> seal_chunks(std::span<const std::uint8_t> plaintext, std::uint8_t *out) noexcept;
Result<std::size_t> seal_one(std::span<const std::uint8_t> plaintext, std::uint8_t *out) noexcept;
Psk psk_{};
Role role_ = Role::client;
const Aead2022TcpDecryptor *peer_ = nullptr;
AeadEngine engine_;
std::array<std::uint8_t, kMaxKey> salt_{};
std::array<std::uint8_t, kMaxNonce> nonce_{};
bool header_sent_ = false;
};
class Aead2022TcpDecryptor {
public:
// A client decryptor checks the response echoes `request_salt`; a server
// decryptor rejects salts already in `pool` (which may be null in tests).
Status init(const Psk &psk, Role role, std::span<const std::uint8_t> request_salt = {},
SaltPool *pool = nullptr) noexcept;
struct Step {
std::size_t consumed;
std::size_t produced;
};
// Like AeadTcpDecryptor::decrypt; `out` must hold at least k2022MaxChunk
// bytes. A server's first output is the SOCKS target address followed by
// the initial payload, with the padding removed.
Result<Step> decrypt(std::span<const std::uint8_t> in, std::span<std::uint8_t> out) noexcept;
bool header_received() const noexcept { return stage_ != Stage::fixed; }
std::span<const std::uint8_t> salt() const noexcept { return { salt_.data(), psk_.size() }; }
std::size_t pending() const noexcept { return buffer_.size(); }
private:
enum class Stage : std::uint8_t { fixed, variable, length, payload };
Psk psk_{};
Role role_ = Role::server;
SaltPool *pool_ = nullptr;
AeadEngine engine_;
std::array<std::uint8_t, kMaxKey> salt_{};
std::array<std::uint8_t, kMaxKey> request_salt_{};
std::array<std::uint8_t, kMaxNonce> nonce_{};
// The largest unit is a full 64 KiB chunk plus its tag.
FixedBuffer<k2022MaxChunk + kTag + 64> buffer_;
std::size_t want_ = 0; // plaintext length of the next variable/payload chunk
Stage stage_ = Stage::fixed;
bool failed_ = false;
};
// One end of a UDP conversation: our outgoing session and the replay window
// of the peer session we hear from. Callers own the storage.
struct UdpSession2022 {
std::array<std::uint8_t, kSessionIdLen> tx_id{};
std::uint64_t tx_packet = 0;
std::array<std::uint8_t, kMaxKey> tx_subkey{};
bool tx_ready = false;
std::array<std::uint8_t, kSessionIdLen> rx_id{};
std::uint64_t rx_highest = 0;
std::array<std::uint8_t, kUdpWindowBits / 8> rx_window{};
bool rx_ready = false;
// Server side: the client session ID replies are addressed to.
std::array<std::uint8_t, kSessionIdLen> peer_id{};
bool peer_known = false;
~UdpSession2022();
};
// Encrypt [SOCKS address][payload] into one packet; returns its length.
// `out` must hold udp2022_overhead() more bytes than the plaintext.
Result<std::size_t> udp2022_seal(const Psk &psk, Role role, UdpSession2022 &session,
std::span<const std::uint8_t> plaintext, std::span<std::uint8_t> out) noexcept;
// Receiving is two steps, because a server finds the session by the ID
// inside the packet: decrypt authenticates the packet into `out` (at least
// as large as the packet) and names its session, then accept checks the
// header and replay window against that session and leaves
// [SOCKS address][payload] at the start of `out`, returning its length.
struct Udp2022Packet {
std::array<std::uint8_t, kSessionIdLen> session_id;
std::uint64_t packet_id;
std::size_t body_len;
};
Result<Udp2022Packet> udp2022_decrypt(const Psk &psk, std::span<const std::uint8_t> packet,
std::span<std::uint8_t> out) noexcept;
Result<std::size_t> udp2022_accept(Role role, UdpSession2022 &session, const Udp2022Packet &packet,
std::span<std::uint8_t> out, std::uint64_t now) noexcept;
std::size_t udp2022_overhead(const Psk &psk, Role role) noexcept;
} // namespace ss::crypto
#endif
+10
View File
@@ -222,6 +222,12 @@ 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)
# test_crypto_aead2022_cpp - C++ SIP022 codecs, differentially against aead2022.cpp
ss_add_test(test_crypto_aead2022_cpp
"test_crypto_aead2022_cpp.cpp;${PROJECT_SOURCE_DIR}/src/crypto/aead2022.cpp;${PROJECT_SOURCE_DIR}/src/crypto/aead.cpp;${SS_CRYPTO_TEST_SOURCES}"
"${TEST_CRYPTO_LIBS};${SS_MATH_LIBRARY}")
ss_strict_warnings(test_crypto_aead2022_cpp.cpp ${PROJECT_SOURCE_DIR}/src/crypto/aead2022.cpp)
if(SS_ENABLE_LEGACY)
# test_crypto_stream_cpp - C++ stream codecs, differentially against stream.cpp
ss_add_test(test_crypto_stream_cpp
@@ -323,6 +329,10 @@ 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)
add_test(NAME alloc_steady_crypto_aead2022
COMMAND ${Python3_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/alloc_audit/check_steady.py
--shim $<TARGET_FILE:ss_alloc_audit> -- $<TARGET_FILE:test_crypto_aead2022_cpp>)
set_tests_properties(alloc_steady_crypto_aead2022 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
+426
View File
@@ -0,0 +1,426 @@
/* SPDX-License-Identifier: GPL-3.0-or-later */
// The C++ SIP022 codecs (src/crypto/aead2022.hpp) differentially against the
// C implementation in aead2022.cpp / aead2022_udp.cpp, in both roles, plus
// replay protection and a zero-allocation steady phase.
#include <algorithm>
#include <cstdint>
#include <cstring>
#include <ctime>
#include <vector>
#include <sodium.h>
#include "alloc_audit/alloc_probe.hpp"
#include "crypto/aead2022.hpp"
#include "test_helpers.hpp"
#include "aead.h"
#include "crypto.h"
#include "ppbloom.h"
#include "utils.h"
#undef min
#undef max
extern "C" {
int verbose = 0;
struct cache *nonce_cache = nullptr;
}
namespace sc = ss::crypto;
namespace {
const char kPsk16[] = "AAECAwQFBgcICQoLDA0ODw==";
const char kPsk32[] = "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=";
const char *const kMethods[] = { "2022-blake3-aes-128-gcm", "2022-blake3-aes-256-gcm",
"2022-blake3-chacha20-poly1305" };
const char *psk_for(const char *method)
{
return std::strcmp(method, "2022-blake3-aes-128-gcm") == 0 ? kPsk16 : kPsk32;
}
sc::Psk psk_of(const char *method)
{
auto spec = sc::find_aead2022(method);
SS_CHECK(spec.has_value());
auto psk = sc::Psk::from_base64(*spec, psk_for(method));
SS_CHECK(psk);
return *psk;
}
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(psk_for(method), nullptr, method);
SS_CHECK(crypto != nullptr);
}
~CMethod()
{
ppbloom_free();
ss_free(crypto->cipher);
ss_free(crypto);
}
};
// A C cipher context of a fixed role.
struct CCtx {
crypto_t *crypto;
cipher_ctx_t ctx{};
CCtx(crypto_t *c, int enc, int role) : crypto(c)
{
aead_set_role(role);
c->ctx_init(c->cipher, &ctx, enc);
ctx.role = role;
}
~CCtx() { crypto->ctx_release(&ctx); }
};
const std::uint8_t kAddr[] = { 0x03, 11, 'e', 'x', 'a', 'm', 'p', 'l', 'e', '.', 'c', 'o', 'm', 0x01, 0xBB };
// Run C ciphertext through a C++ decryptor in random slices.
std::vector<std::uint8_t> cpp_decrypt_all(sc::Aead2022TcpDecryptor &dec, const std::vector<std::uint8_t> &wire)
{
std::vector<std::uint8_t> got;
std::vector<std::uint8_t> out(sc::k2022MaxChunk);
for (std::size_t off = 0; off < wire.size();) {
std::size_t n = std::min<std::size_t>(1 + randombytes_uniform(9000), wire.size() - off);
std::span<const std::uint8_t> in(wire.data() + off, n);
for (;;) {
auto step = dec.decrypt(in, out);
SS_CHECK(step);
got.insert(got.end(), out.begin(), out.begin() + step->produced);
in = in.subspan(step->consumed);
if (step->consumed == 0 && step->produced == 0) {
break;
}
}
off += n;
}
return got;
}
std::vector<std::uint8_t> c_encrypt(CCtx &enc, std::span<const std::uint8_t> plain)
{
const std::size_t cap = 4 * 65536;
buffer_t buf{};
SS_CHECK(balloc(&buf, cap) > 0);
std::memcpy(buf.data, plain.data(), plain.size());
buf.len = plain.size();
SS_CHECK(enc.crypto->encrypt(&buf, &enc.ctx, cap) == CRYPTO_OK);
std::vector<std::uint8_t> wire(buf.data, buf.data + buf.len);
bfree(&buf);
return wire;
}
std::vector<std::uint8_t> c_decrypt(CCtx &dec, std::span<const std::uint8_t> wire)
{
const std::size_t cap = 4 * 65536;
std::vector<std::uint8_t> got;
buffer_t buf{};
SS_CHECK(balloc(&buf, cap) > 0);
for (std::size_t off = 0; off < wire.size();) {
std::size_t n = std::min<std::size_t>(1 + randombytes_uniform(9000), wire.size() - off);
std::memcpy(buf.data, wire.data() + off, n);
buf.len = n;
int rc = dec.crypto->decrypt(&buf, &dec.ctx, cap);
SS_CHECK(rc == CRYPTO_OK || rc == CRYPTO_NEED_MORE);
if (rc == CRYPTO_OK) {
got.insert(got.end(), buf.data, buf.data + buf.len);
}
off += n;
}
bfree(&buf);
return got;
}
std::vector<std::uint8_t> cpp_seal(sc::Aead2022TcpEncryptor &enc, std::span<const std::uint8_t> plain)
{
std::vector<std::uint8_t> wire(enc.sealed_size(plain.size()));
auto n = enc.seal(plain, wire);
SS_CHECK(n);
wire.resize(*n);
return wire;
}
// C client -> C++ server, then C++ server -> C client.
void tcp_c_client(const char *method)
{
CMethod c(method);
sc::Psk psk = psk_of(method);
CCtx enc(c.crypto, 1, CRYPTO_ROLE_CLIENT);
CCtx dec(c.crypto, 0, CRYPTO_ROLE_CLIENT);
cipher_ctx_pair(&enc.ctx, &dec.ctx);
auto body = random_bytes(200000); // spans several 64 KiB chunks
std::vector<std::uint8_t> first(kAddr, kAddr + sizeof(kAddr));
first.insert(first.end(), body.begin(), body.begin() + 100);
auto wire = c_encrypt(enc, first);
auto more = c_encrypt(enc, { body.data() + 100, body.size() - 100 });
wire.insert(wire.end(), more.begin(), more.end());
sc::SaltPool pool;
SS_CHECK(pool.reserve(64, 1));
sc::Aead2022TcpDecryptor sdec;
SS_CHECK(sdec.init(psk, sc::Role::server, {}, &pool));
auto got = cpp_decrypt_all(sdec, wire);
std::vector<std::uint8_t> want(kAddr, kAddr + sizeof(kAddr));
want.insert(want.end(), body.begin(), body.end());
SS_CHECK(got == want);
sc::Aead2022TcpEncryptor senc;
SS_CHECK(senc.init(psk, sc::Role::server, &sdec));
auto reply = random_bytes(150000);
auto rwire = cpp_seal(senc, reply);
auto rest = cpp_seal(senc, { reply.data(), 10 });
rwire.insert(rwire.end(), rest.begin(), rest.end());
auto rgot = c_decrypt(dec, rwire);
reply.insert(reply.end(), reply.begin(), reply.begin() + 10);
SS_CHECK(rgot == reply);
// Replaying the request is refused by the salt pool.
sc::Aead2022TcpDecryptor again;
SS_CHECK(again.init(psk, sc::Role::server, {}, &pool));
std::vector<std::uint8_t> out(sc::k2022MaxChunk);
auto step = again.decrypt(wire, out);
SS_CHECK(!step && step.error().code == ss::Errc::crypto);
}
// C++ client -> C server, then C server -> C++ client.
void tcp_cpp_client(const char *method)
{
CMethod c(method);
sc::Psk psk = psk_of(method);
CCtx dec(c.crypto, 0, CRYPTO_ROLE_SERVER);
CCtx enc(c.crypto, 1, CRYPTO_ROLE_SERVER);
cipher_ctx_pair(&enc.ctx, &dec.ctx);
sc::Aead2022TcpEncryptor cenc;
SS_CHECK(cenc.init(psk, sc::Role::client));
// The address alone: the request header gets padding.
auto wire = cpp_seal(cenc, kAddr);
auto body = random_bytes(70000);
auto more = cpp_seal(cenc, body);
wire.insert(wire.end(), more.begin(), more.end());
auto got = c_decrypt(dec, wire);
std::vector<std::uint8_t> want(kAddr, kAddr + sizeof(kAddr));
want.insert(want.end(), body.begin(), body.end());
SS_CHECK(got == want);
sc::Aead2022TcpDecryptor cdec;
SS_CHECK(cdec.init(psk, sc::Role::client, cenc.salt()));
auto reply = random_bytes(5000);
auto rwire = c_encrypt(enc, reply);
SS_CHECK(cpp_decrypt_all(cdec, rwire) == reply);
// A response that echoes another salt is rejected.
sc::Aead2022TcpEncryptor other;
SS_CHECK(other.init(psk, sc::Role::client));
sc::Aead2022TcpDecryptor wrong;
SS_CHECK(wrong.init(psk, sc::Role::client, other.salt()));
std::vector<std::uint8_t> out(sc::k2022MaxChunk);
SS_CHECK(!wrong.decrypt(rwire, out));
}
void tcp_rejects()
{
sc::Psk psk = psk_of("2022-blake3-aes-256-gcm");
sc::Aead2022TcpEncryptor enc;
SS_CHECK(enc.init(psk, sc::Role::client));
std::uint8_t junk[3] = { 9, 9, 9 };
std::vector<std::uint8_t> out(enc.sealed_size(sizeof(junk)));
SS_CHECK(!enc.seal(junk, out)); // not a SOCKS address
sc::Aead2022TcpDecryptor dec;
SS_CHECK(!enc.init(psk, sc::Role::server, nullptr));
SS_CHECK(dec.init(psk, sc::Role::server));
sc::Aead2022TcpEncryptor server;
SS_CHECK(server.init(psk, sc::Role::server, &dec));
std::uint8_t data[4] = {};
std::vector<std::uint8_t> sout(server.sealed_size(4));
SS_CHECK(!server.seal(data, sout)); // no request yet
// A tampered byte fails authentication.
sc::Aead2022TcpEncryptor client;
SS_CHECK(client.init(psk, sc::Role::client));
auto wire = cpp_seal(client, kAddr);
wire[psk.size() + 3] ^= 1;
std::vector<std::uint8_t> plain(sc::k2022MaxChunk);
auto step = dec.decrypt(wire, plain);
SS_CHECK(!step && step.error().code == ss::Errc::crypto);
}
void udp(const char *method)
{
CMethod c(method);
sc::Psk psk = psk_of(method);
const std::size_t cap = 65536;
std::vector<std::uint8_t> plain(kAddr, kAddr + sizeof(kAddr));
auto payload = random_bytes(1200);
plain.insert(plain.end(), payload.begin(), payload.end());
// C client -> C++ server -> C client.
aead_set_role(CRYPTO_ROLE_CLIENT);
void *slot = nullptr;
buffer_t buf{};
SS_CHECK(balloc(&buf, cap) > 0);
std::memcpy(buf.data, plain.data(), plain.size());
buf.len = plain.size();
SS_CHECK(c.crypto->encrypt_udp(&buf, c.crypto->cipher, cap, &slot) == CRYPTO_OK);
sc::UdpSession2022 server;
std::vector<std::uint8_t> out(buf.len);
auto pkt = sc::udp2022_decrypt(psk, { reinterpret_cast<std::uint8_t *>(buf.data), buf.len }, out);
SS_CHECK(pkt);
auto n = sc::udp2022_accept(sc::Role::server, server, *pkt, out, std::time(nullptr));
SS_CHECK(n && *n == plain.size() && std::memcmp(out.data(), plain.data(), plain.size()) == 0);
// The same packet again is a replay.
auto pkt2 = sc::udp2022_decrypt(psk, { reinterpret_cast<std::uint8_t *>(buf.data), buf.len }, out);
SS_CHECK(pkt2 && !sc::udp2022_accept(sc::Role::server, server, *pkt2, out, std::time(nullptr)));
std::vector<std::uint8_t> reply(plain.size() + sc::udp2022_overhead(psk, sc::Role::server));
auto w = sc::udp2022_seal(psk, sc::Role::server, server, plain, reply);
SS_CHECK(w && *w == reply.size());
std::memcpy(buf.data, reply.data(), reply.size());
buf.len = reply.size();
SS_CHECK(c.crypto->decrypt_udp(&buf, c.crypto->cipher, cap, &slot) == CRYPTO_OK);
SS_CHECK(buf.len == plain.size() - 0 || buf.len == plain.size());
SS_CHECK(std::memcmp(buf.data, plain.data(), plain.size()) == 0);
c.crypto->udp_session_release(slot);
slot = nullptr;
// C++ client -> C server -> C++ client.
sc::UdpSession2022 client;
std::vector<std::uint8_t> req(plain.size() + sc::udp2022_overhead(psk, sc::Role::client));
w = sc::udp2022_seal(psk, sc::Role::client, client, plain, req);
SS_CHECK(w);
aead_set_role(CRYPTO_ROLE_SERVER);
std::memcpy(buf.data, req.data(), *w);
buf.len = *w;
SS_CHECK(c.crypto->decrypt_udp(&buf, c.crypto->cipher, cap, &slot) == CRYPTO_OK);
SS_CHECK(buf.len == plain.size() && std::memcmp(buf.data, plain.data(), plain.size()) == 0);
std::memcpy(buf.data, plain.data(), plain.size());
buf.len = plain.size();
SS_CHECK(c.crypto->encrypt_udp(&buf, c.crypto->cipher, cap, &slot) == CRYPTO_OK);
std::vector<std::uint8_t> back(buf.len);
pkt = sc::udp2022_decrypt(psk, { reinterpret_cast<std::uint8_t *>(buf.data), buf.len }, back);
SS_CHECK(pkt);
n = sc::udp2022_accept(sc::Role::client, client, *pkt, back, std::time(nullptr));
SS_CHECK(n && *n == plain.size() && std::memcmp(back.data(), plain.data(), plain.size()) == 0);
c.crypto->udp_session_release(slot);
bfree(&buf);
}
void udp_window()
{
sc::Psk psk = psk_of("2022-blake3-aes-128-gcm");
sc::UdpSession2022 client, server;
std::uint8_t plain[sizeof(kAddr)];
std::memcpy(plain, kAddr, sizeof(kAddr));
std::vector<std::vector<std::uint8_t> > packets;
for (int i = 0; i < 300; ++i) {
std::vector<std::uint8_t> p(sizeof(plain) + sc::udp2022_overhead(psk, sc::Role::client));
SS_CHECK(sc::udp2022_seal(psk, sc::Role::client, client, plain, p));
packets.push_back(p);
}
auto deliver = [&](std::size_t i) {
std::vector<std::uint8_t> out(packets[i].size());
auto pkt = sc::udp2022_decrypt(psk, packets[i], out);
SS_CHECK(pkt && pkt->packet_id == i);
return static_cast<bool>(sc::udp2022_accept(sc::Role::server, server, *pkt, out, std::time(nullptr)));
};
SS_CHECK(deliver(5) && deliver(0) && deliver(3) && !deliver(3));
SS_CHECK(deliver(299));
SS_CHECK(!deliver(10)); // fell out of the 256-packet window
SS_CHECK(deliver(100) && !deliver(100));
// Stale timestamps are refused.
std::vector<std::uint8_t> p(sizeof(plain) + sc::udp2022_overhead(psk, sc::Role::client));
SS_CHECK(sc::udp2022_seal(psk, sc::Role::client, client, plain, p));
std::vector<std::uint8_t> out(p.size());
auto pkt = sc::udp2022_decrypt(psk, p, out);
SS_CHECK(pkt && !sc::udp2022_accept(sc::Role::server, server, *pkt, out, std::time(nullptr) + 31));
}
void salt_pool()
{
sc::SaltPool pool;
SS_CHECK(pool.reserve(2, 7));
std::uint8_t a[32] = { 1 }, b[32] = { 2 }, d[32] = { 3 };
SS_CHECK(pool.check_and_add(a, 100) && !pool.check_and_add(a, 159));
SS_CHECK(pool.check_and_add(a, 160)); // retention over
SS_CHECK(pool.check_and_add(b, 161) && pool.check_and_add(d, 161) && pool.size() == 2);
}
void steady(const sc::Psk &psk, sc::SaltPool &pool)
{
static std::uint8_t plain[16384], wire[16384 + 2048], out[sc::k2022MaxChunk];
std::memcpy(plain, kAddr, sizeof(kAddr));
for (int conn = 0; conn < 50; ++conn) {
sc::Aead2022TcpEncryptor cenc;
sc::Aead2022TcpDecryptor sdec;
SS_CHECK(cenc.init(psk, sc::Role::client) && sdec.init(psk, sc::Role::server, {}, &pool));
for (std::size_t n : { sizeof(kAddr) + 1, (std::size_t)100, (std::size_t)16000 }) {
auto w = cenc.seal({ plain, n }, wire);
SS_CHECK(w);
auto step = sdec.decrypt({ wire, *w }, out);
SS_CHECK(step && step->consumed == *w);
}
sc::UdpSession2022 cs, ss2;
for (int i = 0; i < 3; ++i) {
auto p = sc::udp2022_seal(psk, sc::Role::client, cs, { plain, 1200 }, wire);
SS_CHECK(p);
auto pkt = sc::udp2022_decrypt(psk, { wire, *p }, out);
SS_CHECK(pkt && sc::udp2022_accept(sc::Role::server, ss2, *pkt, out, std::time(nullptr)));
}
}
}
void test_methods()
{
for (const char *m : kMethods) {
tcp_c_client(m);
tcp_cpp_client(m);
udp(m);
std::printf("ok %s\n", m);
}
tcp_rejects();
udp_window();
salt_pool();
SS_CHECK(!sc::find_aead2022("aes-256-gcm"));
auto spec = sc::find_aead2022("2022-blake3-aes-256-gcm");
SS_CHECK(!sc::Psk::from_base64(*spec, kPsk16)); // too short
SS_CHECK(!sc::Psk::from_base64(*spec, "not base64"));
}
void test_no_heap_in_steady_state()
{
sc::SaltPool pool;
SS_CHECK(pool.reserve(4096, 42));
sc::Psk keys[3] = { psk_of(kMethods[0]), psk_of(kMethods[1]), psk_of(kMethods[2]) };
for (const sc::Psk &k : keys) {
steady(k, pool);
}
ss::test::alloc_mark();
for (const sc::Psk &k : keys) {
steady(k, pool);
}
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;
}