Merge pull request #3049 from shadowsocks/feature/aead-2022

Add Shadowsocks 2022 (SIP022) ciphers and ss:// URL support
This commit is contained in:
Max Lv authored and GitHub committed 2026-08-20 09:18:27 +08:00
commit 8fe386b5c2
43 files changed
+4647 -108

No files matched your search

+7 -2
View File
@@ -13,10 +13,15 @@ include(GNUInstallDirs)
# Compilation database for clang-tidy and other tooling
set(CMAKE_EXPORT_COMPILE_COMMANDS ON)
# Compiler flags matching autotools
# C11 with GNU extensions. _GNU_SOURCE stays for the glibc-specific
# interfaces used by the Linux-only modules (ss-redir, connmark/nftables).
set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)
set(CMAKE_C_EXTENSIONS ON)
# Note: -Werror is applied per-target in src/CMakeLists.txt to avoid
# breaking bundled submodules (libcork, libipset, libbloom)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -std=gnu99 -D_GNU_SOURCE")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -D_GNU_SOURCE")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -O2 -Wall -Wno-deprecated-declarations -fno-strict-aliasing")
option(ENABLE_SANITIZERS "Build with AddressSanitizer and UndefinedBehaviorSanitizer" OFF)
+20 -1
View File
@@ -344,7 +344,11 @@ you may refer to the man pages of the applications, respectively.
-k <password> Password of your remote server.
-m <encrypt_method> Encrypt method: rc4-md5,
-m <encrypt_method> Encrypt method:
2022-blake3-aes-128-gcm,
2022-blake3-aes-256-gcm,
2022-blake3-chacha20-poly1305,
rc4-md5,
aes-128-gcm, aes-192-gcm, aes-256-gcm,
aes-128-cfb, aes-192-cfb, aes-256-cfb,
aes-128-ctr, aes-192-ctr, aes-256-ctr,
@@ -355,6 +359,21 @@ you may refer to the man pages of the applications, respectively.
salsa20, chacha20 and chacha20-ietf.
The default cipher is chacha20-ietf-poly1305.
The 2022-blake3-* ciphers implement
Shadowsocks 2022 (SIP022) and are the
recommended choice. They take a
base64-encoded pre-shared key of exactly
the cipher's key size via -k, not a
password: generate one with
`openssl rand -base64 32` (or 16 for
2022-blake3-aes-128-gcm).
[--server-url <ss_url>] Take the server address, port, cipher,
password and SIP003 plugin from a single
ss:// URL (SIP002 or the legacy form).
ss-local only. Options given later on the
command line override the URL's values.
[-a <user>] Run as another user.
[-f <pid_file>] The file path to store pid.
+84 -34
View File
@@ -5,54 +5,79 @@
# MBEDTLS_INCLUDE_DIRS
# MBEDTLS_CRYPTO_LIBRARY
# MBEDTLS_TLS_LIBRARY
#
# The headers and the libraries MUST come from the same installation. Mixing
# them is not a build-time error but a memory-corruption bug at runtime:
# mbedTLS structs such as mbedtls_cipher_context_t changed size between 3.x
# and 4.x, so a context allocated with sizeof() from one version and
# initialized by the other overruns its heap block. Homebrew makes this easy
# to hit, since mbedtls (4.x) and the keg-only mbedtls@3 can both be present
# and CMAKE_PREFIX_PATH outranks HINTS in find_library's search order.
include(CheckCSourceCompiles)
# mbedtls@3 is keg-only on Homebrew; also check versioned opt paths
find_path(MBEDTLS_INCLUDE_DIR
NAMES mbedtls/cipher.h
HINTS
/opt/homebrew/opt/mbedtls@3/include
/usr/local/opt/mbedtls@3/include
/opt/homebrew/opt/mbedtls/include
/usr/local/opt/mbedtls/include
/opt/homebrew/include
/usr/local/include
/usr/include
# This codebase targets the mbedTLS 3.x API, so prefer a 3.x installation.
set(_MBEDTLS_PREFIX_HINTS
/opt/homebrew/opt/mbedtls@3
/usr/local/opt/mbedtls@3
/opt/homebrew/opt/mbedtls
/usr/local/opt/mbedtls
/opt/homebrew
/usr/local
/usr
)
find_library(MBEDTLS_CRYPTO_LIBRARY
NAMES mbedcrypto
HINTS
/opt/homebrew/opt/mbedtls@3/lib
/usr/local/opt/mbedtls@3/lib
/opt/homebrew/opt/mbedtls/lib
/usr/local/opt/mbedtls/lib
/opt/homebrew/lib
/usr/local/lib
/usr/lib
)
# Locate a prefix that provides both the headers and libmbedcrypto, so the
# two can never be drawn from different installations.
foreach(_prefix IN LISTS _MBEDTLS_PREFIX_HINTS)
if(EXISTS "${_prefix}/include/mbedtls/cipher.h")
find_library(_MBEDTLS_CRYPTO_IN_PREFIX
NAMES mbedcrypto
PATHS "${_prefix}/lib"
NO_DEFAULT_PATH
)
if(_MBEDTLS_CRYPTO_IN_PREFIX)
set(_MBEDTLS_ROOT "${_prefix}")
break()
endif()
unset(_MBEDTLS_CRYPTO_IN_PREFIX CACHE)
endif()
endforeach()
find_library(MBEDTLS_TLS_LIBRARY
NAMES mbedtls
HINTS
/opt/homebrew/opt/mbedtls@3/lib
/usr/local/opt/mbedtls@3/lib
/opt/homebrew/opt/mbedtls/lib
/usr/local/opt/mbedtls/lib
/opt/homebrew/lib
/usr/local/lib
/usr/lib
)
if(_MBEDTLS_ROOT)
# Pin every component to the prefix chosen above.
find_path(MBEDTLS_INCLUDE_DIR
NAMES mbedtls/cipher.h
PATHS "${_MBEDTLS_ROOT}/include"
NO_DEFAULT_PATH
)
find_library(MBEDTLS_CRYPTO_LIBRARY
NAMES mbedcrypto
PATHS "${_MBEDTLS_ROOT}/lib"
NO_DEFAULT_PATH
)
find_library(MBEDTLS_TLS_LIBRARY
NAMES mbedtls
PATHS "${_MBEDTLS_ROOT}/lib"
NO_DEFAULT_PATH
)
else()
# No single prefix provided both; fall back to a plain search.
find_path(MBEDTLS_INCLUDE_DIR NAMES mbedtls/cipher.h)
find_library(MBEDTLS_CRYPTO_LIBRARY NAMES mbedcrypto)
find_library(MBEDTLS_TLS_LIBRARY NAMES mbedtls)
endif()
unset(_MBEDTLS_CRYPTO_IN_PREFIX CACHE)
if(MBEDTLS_INCLUDE_DIR AND MBEDTLS_CRYPTO_LIBRARY)
set(MBEDTLS_FOUND TRUE)
set(MBEDTLS_INCLUDE_DIRS ${MBEDTLS_INCLUDE_DIR})
# Check for required CFB mode support
set(CMAKE_REQUIRED_INCLUDES ${MBEDTLS_INCLUDE_DIR})
set(CMAKE_REQUIRED_LIBRARIES ${MBEDTLS_CRYPTO_LIBRARY})
# Check for required CFB mode support
check_c_source_compiles("
#include <mbedtls/cipher.h>
#if !defined(MBEDTLS_CIPHER_MODE_CFB)
@@ -78,6 +103,31 @@ if(MBEDTLS_INCLUDE_DIR AND MBEDTLS_CRYPTO_LIBRARY)
endif()
endif()
# Verify the headers and the runtime library agree on the version. A
# mismatch silently corrupts the heap, so refuse to configure instead.
if(NOT CMAKE_CROSSCOMPILING AND NOT DEFINED MBEDTLS_VERSION_CONSISTENT)
try_run(_mbedtls_run_result _mbedtls_compile_result
${CMAKE_BINARY_DIR}/CMakeFiles/mbedtls_version_check
${CMAKE_CURRENT_LIST_DIR}/mbedtls_version_check.c
CMAKE_FLAGS
"-DINCLUDE_DIRECTORIES=${MBEDTLS_INCLUDE_DIR}"
"-DLINK_LIBRARIES=${MBEDTLS_CRYPTO_LIBRARY}"
RUN_OUTPUT_VARIABLE _mbedtls_run_output
)
if(_mbedtls_compile_result AND _mbedtls_run_result EQUAL 0)
set(MBEDTLS_VERSION_CONSISTENT TRUE CACHE INTERNAL "")
elseif(_mbedtls_compile_result)
message(FATAL_ERROR
"mbedTLS headers and library are from different installations:\n"
" ${_mbedtls_run_output}\n"
" headers: ${MBEDTLS_INCLUDE_DIR}\n"
" library: ${MBEDTLS_CRYPTO_LIBRARY}\n"
"Mixing them corrupts memory at runtime. Point both at the same "
"mbedTLS installation, e.g. "
"-DCMAKE_PREFIX_PATH=/opt/homebrew/opt/mbedtls@3")
endif()
endif()
unset(CMAKE_REQUIRED_INCLUDES)
unset(CMAKE_REQUIRED_LIBRARIES)
+29
View File
@@ -0,0 +1,29 @@
/*
* Configure-time check that the mbedTLS headers and the linked library are
* the same version. A mismatch is not a link error, but struct layouts
* differ across major versions, so contexts sized with sizeof() from the
* headers are overrun by library code that expects the other layout.
*
* Exits 0 when the compile-time and runtime versions agree.
*/
#include <stdio.h>
#include <string.h>
#include <mbedtls/version.h>
int
main(void)
{
char runtime[32] = { 0 };
mbedtls_version_get_string(runtime);
if (strcmp(runtime, MBEDTLS_VERSION_STRING) != 0) {
printf("headers say %s, library reports %s",
MBEDTLS_VERSION_STRING, runtime);
return 1;
}
return 0;
}
+10 -1
View File
@@ -56,8 +56,10 @@ Not available in manager mode.
-m <encrypt_method>::
Set the cipher.
+
*Shadowsocks-libev* accepts 19 different ciphers:
*Shadowsocks-libev* accepts 22 different ciphers:
+
2022-blake3-aes-128-gcm, 2022-blake3-aes-256-gcm,
2022-blake3-chacha20-poly1305,
aes-128-gcm, aes-192-gcm, aes-256-gcm,
rc4-md5, aes-128-cfb, aes-192-cfb, aes-256-cfb,
aes-128-ctr, aes-192-ctr, aes-256-ctr, bf-cfb,
@@ -67,6 +69,13 @@ salsa20, chacha20 and chacha20-ietf.
+
The default cipher is 'chacha20-ietf-poly1305'.
+
The '2022-blake3-*' ciphers implement Shadowsocks 2022 (SIP022) and are
the recommended choice: they provide full replay protection and
session-based UDP relay. They require a base64-encoded pre-shared key of
exactly the cipher's key size (16 bytes for 2022-blake3-aes-128-gcm, 32
bytes for the others) given with *-k*; passwords are not accepted and are
never stretched into a key. Generate one with `openssl rand -base64 32`.
+
If built with PolarSSL or custom OpenSSL libraries, some of
these ciphers may not work.
+19 -1
View File
@@ -17,6 +17,7 @@ SYNOPSIS
[--mtu <MTU>] [--no-delay]
[--plugin <plugin_name>] [--plugin-opts <plugin_options>]
[--password <password>] [--key <key_in_base64>]
[--server-url <ss_url>]
DESCRIPTION
-----------
@@ -48,11 +49,21 @@ Set the password. The server and the client should use the same password.
--key <key_in_base64>::
Set the key directly. The key should be encoded with URL-safe Base64.
--server-url <ss_url>::
Take the server address, port, cipher, password and any SIP003 plugin
from a single 'ss://' URL, as produced by most clients and by
*shadowsocks-rust*'s `ssurl`. Both the SIP002 form
('ss://base64(method:password)@host:port/?plugin=...#tag') and the older
'ss://base64(method:password@host:port)' form are accepted. Options given
later on the command line override the values taken from the URL.
-m <encrypt_method>::
Set the cipher.
+
*Shadowsocks-libev* accepts 19 different ciphers:
*Shadowsocks-libev* accepts 22 different ciphers:
+
2022-blake3-aes-128-gcm, 2022-blake3-aes-256-gcm,
2022-blake3-chacha20-poly1305,
aes-128-gcm, aes-192-gcm, aes-256-gcm,
rc4-md5, aes-128-cfb, aes-192-cfb, aes-256-cfb,
aes-128-ctr, aes-192-ctr, aes-256-ctr, bf-cfb,
@@ -62,6 +73,13 @@ salsa20, chacha20 and chacha20-ietf.
+
The default cipher is 'chacha20-ietf-poly1305'.
+
The '2022-blake3-*' ciphers implement Shadowsocks 2022 (SIP022) and are
the recommended choice: they provide full replay protection and
session-based UDP relay. They require a base64-encoded pre-shared key of
exactly the cipher's key size (16 bytes for 2022-blake3-aes-128-gcm, 32
bytes for the others) given with *-k*; passwords are not accepted and are
never stretched into a key. Generate one with `openssl rand -base64 32`.
+
If built with PolarSSL or custom OpenSSL libraries, some of
these ciphers may not work.
+10 -1
View File
@@ -43,8 +43,10 @@ Set the password. The server and the client should use the same password.
-m <encrypt_method>::
Set the cipher.
+
*Shadowsocks-libev* accepts 19 different ciphers:
*Shadowsocks-libev* accepts 22 different ciphers:
+
2022-blake3-aes-128-gcm, 2022-blake3-aes-256-gcm,
2022-blake3-chacha20-poly1305,
aes-128-gcm, aes-192-gcm, aes-256-gcm,
rc4-md5, aes-128-cfb, aes-192-cfb, aes-256-cfb,
aes-128-ctr, aes-192-ctr, aes-256-ctr, bf-cfb,
@@ -54,6 +56,13 @@ salsa20, chacha20 and chacha20-ietf.
+
The default cipher is 'chacha20-ietf-poly1305'.
+
The '2022-blake3-*' ciphers implement Shadowsocks 2022 (SIP022) and are
the recommended choice: they provide full replay protection and
session-based UDP relay. They require a base64-encoded pre-shared key of
exactly the cipher's key size (16 bytes for 2022-blake3-aes-128-gcm, 32
bytes for the others) given with *-k*; passwords are not accepted and are
never stretched into a key. Generate one with `openssl rand -base64 32`.
+
If built with PolarSSL or custom OpenSSL libraries, some of
these ciphers may not work.
+10 -1
View File
@@ -50,8 +50,10 @@ Set the key directly. The key should be encoded with URL-safe Base64.
-m <encrypt_method>::
Set the cipher.
+
*Shadowsocks-libev* accepts 19 different ciphers:
*Shadowsocks-libev* accepts 22 different ciphers:
+
2022-blake3-aes-128-gcm, 2022-blake3-aes-256-gcm,
2022-blake3-chacha20-poly1305,
aes-128-gcm, aes-192-gcm, aes-256-gcm,
rc4-md5, aes-128-cfb, aes-192-cfb, aes-256-cfb,
aes-128-ctr, aes-192-ctr, aes-256-ctr, bf-cfb,
@@ -61,6 +63,13 @@ salsa20, chacha20 and chacha20-ietf.
+
The default cipher is 'chacha20-ietf-poly1305'.
+
The '2022-blake3-*' ciphers implement Shadowsocks 2022 (SIP022) and are
the recommended choice: they provide full replay protection and
session-based UDP relay. They require a base64-encoded pre-shared key of
exactly the cipher's key size (16 bytes for 2022-blake3-aes-128-gcm, 32
bytes for the others) given with *-k*; passwords are not accepted and are
never stretched into a key. Generate one with `openssl rand -base64 32`.
+
If built with PolarSSL or custom OpenSSL libraries, some of
these ciphers may not work.
+10 -1
View File
@@ -48,8 +48,10 @@ Set the key directly. The key should be encoded with URL-safe Base64.
-m <encrypt_method>::
Set the cipher.
+
*Shadowsocks-libev* accepts 19 different ciphers:
*Shadowsocks-libev* accepts 22 different ciphers:
+
2022-blake3-aes-128-gcm, 2022-blake3-aes-256-gcm,
2022-blake3-chacha20-poly1305,
aes-128-gcm, aes-192-gcm, aes-256-gcm,
rc4-md5, aes-128-cfb, aes-192-cfb, aes-256-cfb,
aes-128-ctr, aes-192-ctr, aes-256-ctr, bf-cfb,
@@ -57,6 +59,13 @@ camellia-128-cfb, camellia-192-cfb, camellia-256-cfb,
chacha20-ietf-poly1305, xchacha20-ietf-poly1305,
salsa20, chacha20 and chacha20-ietf.
+
The '2022-blake3-*' ciphers implement Shadowsocks 2022 (SIP022) and are
the recommended choice: they provide full replay protection and
session-based UDP relay. They require a base64-encoded pre-shared key of
exactly the cipher's key size (16 bytes for 2022-blake3-aes-128-gcm, 32
bytes for the others) given with *-k*; passwords are not accepted and are
never stretched into a key. Generate one with `openssl rand -base64 32`.
+
If built with PolarSSL or custom OpenSSL libraries, some of
these ciphers may not work.
+10 -1
View File
@@ -50,8 +50,10 @@ Set the key directly. The key should be encoded with URL-safe Base64.
-m <encrypt_method>::
Set the cipher.
+
*Shadowsocks-libev* accepts 19 different ciphers:
*Shadowsocks-libev* accepts 22 different ciphers:
+
2022-blake3-aes-128-gcm, 2022-blake3-aes-256-gcm,
2022-blake3-chacha20-poly1305,
aes-128-gcm, aes-192-gcm, aes-256-gcm,
rc4-md5, aes-128-cfb, aes-192-cfb, aes-256-cfb,
aes-128-ctr, aes-192-ctr, aes-256-ctr, bf-cfb,
@@ -61,6 +63,13 @@ salsa20, chacha20 and chacha20-ietf.
+
The default cipher is 'chacha20-ietf-poly1305'.
+
The '2022-blake3-*' ciphers implement Shadowsocks 2022 (SIP022) and are
the recommended choice: they provide full replay protection and
session-based UDP relay. They require a base64-encoded pre-shared key of
exactly the cipher's key size (16 bytes for 2022-blake3-aes-128-gcm, 32
bytes for the others) given with *-k*; passwords are not accepted and are
never stretched into a key. Generate one with `openssl rand -base64 32`.
+
If built with PolarSSL or custom OpenSSL libraries, some of
these ciphers may not work.
+23
View File
@@ -16,10 +16,29 @@ set(SS_ACL_SOURCE
set(SS_CRYPTO_SOURCE
crypto.c
aead.c
aead2022.c
aead2022_udp.c
stream.c
base64.c
)
# BLAKE3 (vendored) provides the key derivation for the 2022 ciphers. Only
# the portable backend is built: the SIMD ones need per-file ISA flags and
# extra assembly, which is not worth it for key derivation.
add_library(blake3 STATIC
blake3/blake3.c
blake3/blake3_dispatch.c
blake3/blake3_portable.c
)
target_compile_definitions(blake3 PUBLIC
BLAKE3_NO_AVX512 BLAKE3_NO_AVX2 BLAKE3_NO_SSE41 BLAKE3_NO_SSE2
BLAKE3_USE_NEON=0
)
target_include_directories(blake3 PUBLIC ${CMAKE_CURRENT_SOURCE_DIR})
# vendored upstream code is not held to the project's -Werror budget
target_compile_options(blake3 PRIVATE -Wno-error -Wno-unused-function)
set_target_properties(blake3 PROPERTIES POSITION_INDEPENDENT_CODE ON)
set(SS_PLUGIN_SOURCE
plugin.c
)
@@ -40,6 +59,7 @@ set(LIBSHADOWSOCKS_LIBEV_SOURCE
udprelay.c
cache.c
local.c
ssurl.c
${SS_CRYPTO_SOURCE}
${SS_PLUGIN_SOURCE}
${SS_ACL_SOURCE}
@@ -111,6 +131,7 @@ find_library(LIBPCRE2 libpcre2-8.a)
# Dependencies we need for static and shared
list(APPEND DEPS
m
blake3
bloom
${LIBEV}
${LIBUDNS}
@@ -138,6 +159,7 @@ find_library(LIBPCRE2_SHARED pcre2-8)
if (WITH_EMBEDDED_SRC)
list(APPEND DEPS_SHARED
m
blake3
bloom
cork
ipset
@@ -154,6 +176,7 @@ find_library(LIBCORK_SHARED cork)
find_library(LIBCORKIPSET_SHARED corkipset)
list(APPEND DEPS_SHARED
m
blake3
${LIBBLOOM_SHARED}
${LIBCORK_SHARED}
${LIBCORKIPSET_SHARED}
+75 -32
View File
@@ -37,23 +37,11 @@
#include "ppbloom.h"
#include "aead.h"
#include "aead_internal.h"
#include "utils.h"
#include "winsock.h"
#define NONE (-1)
#define AES128GCM 0
#define AES192GCM 1
#define AES256GCM 2
/*
* methods above requires gcm context
* methods below doesn't require it,
* then we need to fake one
*/
#define CHACHA20POLY1305IETF 3
#ifdef FS_HAVE_XCHACHA20IETF
#define XCHACHA20POLY1305IETF 4
#endif
#define CHUNK_SIZE_LEN 2
#define CHUNK_SIZE_MASK 0x3FFF
@@ -108,7 +96,10 @@ const char *supported_aead_ciphers[AEAD_CIPHER_NUM] = {
"aes-128-gcm",
"aes-192-gcm",
"aes-256-gcm",
"2022-blake3-aes-128-gcm",
"2022-blake3-aes-256-gcm",
"chacha20-ietf-poly1305",
"2022-blake3-chacha20-poly1305",
#ifdef FS_HAVE_XCHACHA20IETF
"xchacha20-ietf-poly1305"
#endif
@@ -121,6 +112,9 @@ static const char *supported_aead_ciphers_mbedtls[AEAD_CIPHER_NUM] = {
"AES-128-GCM",
"AES-192-GCM",
"AES-256-GCM",
"AES-128-GCM",
"AES-256-GCM",
CIPHER_UNSUPPORTED,
CIPHER_UNSUPPORTED,
#ifdef FS_HAVE_XCHACHA20IETF
CIPHER_UNSUPPORTED
@@ -128,27 +122,33 @@ static const char *supported_aead_ciphers_mbedtls[AEAD_CIPHER_NUM] = {
};
static const int supported_aead_ciphers_nonce_size[AEAD_CIPHER_NUM] = {
12, 12, 12, 12,
12, 12, 12, 12, 12, 12, 12,
#ifdef FS_HAVE_XCHACHA20IETF
24
#endif
};
static const int supported_aead_ciphers_key_size[AEAD_CIPHER_NUM] = {
16, 24, 32, 32,
16, 24, 32, 16, 32, 32, 32,
#ifdef FS_HAVE_XCHACHA20IETF
32
#endif
};
static const int supported_aead_ciphers_tag_size[AEAD_CIPHER_NUM] = {
16, 16, 16, 16,
16, 16, 16, 16, 16, 16, 16,
#ifdef FS_HAVE_XCHACHA20IETF
16
#endif
};
static int
int
aead_is_2022(const cipher_t *cipher)
{
return cipher != NULL && is_aead_2022(cipher->method);
}
int
aead_cipher_encrypt(cipher_ctx_t *cipher_ctx,
uint8_t *c,
size_t *clen,
@@ -177,6 +177,8 @@ aead_cipher_encrypt(cipher_ctx_t *cipher_ctx,
// Otherwise, just use the mbedTLS one with crappy AES-NI.
case AES192GCM:
case AES128GCM:
case AES128GCM2022:
case AES256GCM2022:
#if MBEDTLS_VERSION_NUMBER < 0x03000000
err = mbedtls_cipher_auth_encrypt(cipher_ctx->evp, n, nlen, ad, adlen,
m, mlen, c, clen, c + mlen, tlen);
@@ -187,6 +189,7 @@ aead_cipher_encrypt(cipher_ctx_t *cipher_ctx,
#endif
break;
case CHACHA20POLY1305IETF:
case CHACHA20POLY1305IETF2022:
err = crypto_aead_chacha20poly1305_ietf_encrypt(c, &long_clen, m, mlen,
ad, adlen, NULL, n, k);
*clen = (size_t)long_clen;
@@ -205,7 +208,7 @@ aead_cipher_encrypt(cipher_ctx_t *cipher_ctx,
return err;
}
static int
int
aead_cipher_decrypt(cipher_ctx_t *cipher_ctx,
uint8_t *p, size_t *plen,
uint8_t *m, size_t mlen,
@@ -230,6 +233,8 @@ aead_cipher_decrypt(cipher_ctx_t *cipher_ctx,
// Otherwise, just use the mbedTLS one with crappy AES-NI.
case AES192GCM:
case AES128GCM:
case AES128GCM2022:
case AES256GCM2022:
#if MBEDTLS_VERSION_NUMBER < 0x03000000
err = mbedtls_cipher_auth_decrypt(cipher_ctx->evp, n, nlen, ad, adlen,
m, mlen - tlen, p, plen, m + mlen - tlen, tlen);
@@ -239,6 +244,7 @@ aead_cipher_decrypt(cipher_ctx_t *cipher_ctx,
#endif
break;
case CHACHA20POLY1305IETF:
case CHACHA20POLY1305IETF2022:
err = crypto_aead_chacha20poly1305_ietf_decrypt(p, &long_plen, NULL, m, mlen,
ad, adlen, n, k);
*plen = (size_t)long_plen; // it's safe to cast 64bit to 32bit length here
@@ -290,21 +296,27 @@ aead_get_cipher_type(int method)
return mbedtls_cipher_info_from_string(mbedtlsname);
}
static void
void
aead_cipher_ctx_set_key(cipher_ctx_t *cipher_ctx, int enc)
{
const digest_type_t *md = mbedtls_md_info_from_string("SHA1");
if (md == NULL) {
FATAL("SHA1 Digest not found in crypto library");
}
if (is_aead_2022(cipher_ctx->cipher->method)) {
ss2022_derive_subkey(cipher_ctx->cipher->key, cipher_ctx->cipher->key_len,
cipher_ctx->salt, cipher_ctx->cipher->key_len,
cipher_ctx->skey, cipher_ctx->cipher->key_len);
} else {
const digest_type_t *md = mbedtls_md_info_from_string("SHA1");
if (md == NULL) {
FATAL("SHA1 Digest not found in crypto library");
}
int err = crypto_hkdf(md,
cipher_ctx->salt, cipher_ctx->cipher->key_len,
cipher_ctx->cipher->key, cipher_ctx->cipher->key_len,
(uint8_t *)SUBKEY_INFO, strlen(SUBKEY_INFO),
cipher_ctx->skey, cipher_ctx->cipher->key_len);
if (err) {
FATAL("Unable to generate subkey");
int err = crypto_hkdf(md,
cipher_ctx->salt, cipher_ctx->cipher->key_len,
cipher_ctx->cipher->key, cipher_ctx->cipher->key_len,
(uint8_t *)SUBKEY_INFO, strlen(SUBKEY_INFO),
cipher_ctx->skey, cipher_ctx->cipher->key_len);
if (err) {
FATAL("Unable to generate subkey");
}
}
memset(cipher_ctx->nonce, 0, cipher_ctx->cipher->nonce_len);
@@ -374,6 +386,7 @@ aead_ctx_init(cipher_t *cipher, cipher_ctx_t *cipher_ctx, int enc)
{
sodium_memzero(cipher_ctx, sizeof(cipher_ctx_t));
cipher_ctx->cipher = cipher;
cipher_ctx->role = aead_get_role();
aead_cipher_ctx_init(cipher_ctx, cipher->method, enc);
@@ -407,6 +420,11 @@ aead_ctx_release(cipher_ctx_t *cipher_ctx)
int
aead_encrypt_all(buffer_t *plaintext, cipher_t *cipher, size_t capacity)
{
if (is_aead_2022(cipher->method)) {
LOGE("AEAD-2022 UDP requires the session interface");
return CRYPTO_ERROR;
}
cipher_ctx_t cipher_ctx;
aead_ctx_init(cipher, &cipher_ctx, 1);
@@ -448,6 +466,11 @@ aead_encrypt_all(buffer_t *plaintext, cipher_t *cipher, size_t capacity)
int
aead_decrypt_all(buffer_t *ciphertext, cipher_t *cipher, size_t capacity)
{
if (is_aead_2022(cipher->method)) {
LOGE("AEAD-2022 UDP requires the session interface");
return CRYPTO_ERROR;
}
size_t salt_len = cipher->key_len;
size_t tag_len = cipher->tag_len;
int err = CRYPTO_OK;
@@ -541,6 +564,9 @@ aead_encrypt(buffer_t *plaintext, cipher_ctx_t *cipher_ctx, size_t capacity)
if (cipher_ctx == NULL)
return CRYPTO_ERROR;
if (is_aead_2022(cipher_ctx->cipher->method))
return ss2022_tcp_encrypt(plaintext, cipher_ctx, capacity);
if (plaintext->len == 0) {
return CRYPTO_OK;
}
@@ -635,6 +661,9 @@ aead_decrypt(buffer_t *ciphertext, cipher_ctx_t *cipher_ctx, size_t capacity)
int err = CRYPTO_OK;
cipher_t *cipher = cipher_ctx->cipher;
if (is_aead_2022(cipher->method))
return ss2022_tcp_decrypt(ciphertext, cipher_ctx, capacity);
size_t salt_len = cipher->key_len;
if (cipher_ctx->chunk == NULL) {
@@ -741,12 +770,26 @@ aead_key_init(int method, const char *pass, const char *key)
FATAL("Cannot initialize cipher");
}
if (key != NULL)
if (is_aead_2022(method)) {
/*
* SIP022 requires a cryptographically-secure fixed-length key,
* provided in base64. EVP_BytesToKey-style password derivation
* MUST NOT be used, so the password (or key) field must hold the
* base64 PSK itself.
*/
const char *psk = (key != NULL) ? key : pass;
if (psk == NULL) {
FATAL("AEAD-2022 ciphers require a base64 pre-shared key");
}
cipher->key_len = crypto_parse_psk(psk, cipher->key,
supported_aead_ciphers_key_size[method]);
} else if (key != NULL) {
cipher->key_len = crypto_parse_key(key, cipher->key,
supported_aead_ciphers_key_size[method]);
else
} else {
cipher->key_len = crypto_derive_key(pass, cipher->key,
supported_aead_ciphers_key_size[method]);
}
if (cipher->key_len == 0) {
FATAL("Cannot generate key and nonce");
+10 -2
View File
@@ -28,9 +28,9 @@
// currently, XCHACHA20POLY1305IETF is not released yet
// XCHACHA20POLY1305 is removed in upstream
#ifdef FS_HAVE_XCHACHA20IETF
#define AEAD_CIPHER_NUM 5
#define AEAD_CIPHER_NUM 8
#else
#define AEAD_CIPHER_NUM 4
#define AEAD_CIPHER_NUM 7
#endif
int aead_encrypt_all(buffer_t *, cipher_t *, size_t);
@@ -44,4 +44,12 @@ void aead_ctx_release(cipher_ctx_t *);
cipher_t *aead_init(const char *pass, const char *key, const char *method);
/* AEAD-2022 (SIP022) */
int aead_is_2022(const cipher_t *cipher);
void aead_set_role(int role); /* override compile-time default; used by tests */
int aead_2022_encrypt_udp(buffer_t *, cipher_t *, size_t, void **);
int aead_2022_decrypt_udp(buffer_t *, cipher_t *, size_t, void **);
void aead_2022_udp_session_release(void *);
#endif // _AEAD_H
+643
View File
@@ -0,0 +1,643 @@
/*
* aead2022.c - Shadowsocks 2022 Edition (SIP022) AEAD ciphers
*
* Copyright (C) 2013 - 2026, Max Lv <max.c.lv@gmail.com>
*
* This file is part of the shadowsocks-libev.
*
* shadowsocks-libev is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* shadowsocks-libev is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with shadowsocks-libev; see the file COPYING. If not, see
* <http://www.gnu.org/licenses/>.
*
* Spec: https://github.com/Shadowsocks-NET/shadowsocks-specs/blob/main/2022-1-shadowsocks-2022-edition.md
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <time.h>
#include <sodium.h>
#include "aead.h"
#include "aead_internal.h"
#include "cache.h"
#include "utils.h"
#include "blake3/blake3.h"
/* SIP022 constants */
#define SS2022_HDR_TYPE_CLIENT 0
#define SS2022_HDR_TYPE_SERVER 1
/* TCP decrypt state machine (cipher_ctx_t.hdr_stage) */
#define SS2022_STAGE_FIXED_HDR 0
#define SS2022_STAGE_VAR_HDR 1 /* request: variable header; response: first payload */
#define SS2022_STAGE_STREAM 2
/* request fixed-length header: type(1) + timestamp(8) + length(2) */
#define SS2022_REQ_FIXED_LEN 11
/* response fixed-length header: type(1) + timestamp(8) + request salt + length(2) */
#define SS2022_RESP_FIXED_LEN(salt_len) (11 + (salt_len))
#define SS2022_FIXED_BUF_LEN (11 + MAX_KEY_LENGTH)
#define SS2022_CHUNK_LEN_SIZE 2
#define SS2022_CHUNK_MASK 0xFFFF
#define SS2022_TAG_LEN 16
#define SS2022_SALT_POOL_CAP 65536
/*
* Role handling. AEAD-2022 is asymmetric: the client encrypts request
* streams and decrypts response streams, the server does the opposite.
* The default follows the module this file is compiled into; unit tests
* override it with aead_set_role().
*/
#ifdef MODULE_REMOTE
static int aead_role = CRYPTO_ROLE_SERVER;
#else
static int aead_role = CRYPTO_ROLE_CLIENT;
#endif
void
aead_set_role(int role)
{
aead_role = role;
}
int
aead_get_role(void)
{
return aead_role;
}
/*
* session_subkey := blake3::derive_key(context, key || salt)
* salt is the per-stream random salt for TCP, or the session ID for UDP.
*/
void
ss2022_derive_subkey(const uint8_t *key, size_t key_len,
const uint8_t *salt, size_t salt_len,
uint8_t *subkey, size_t subkey_len)
{
uint8_t material[2 * MAX_KEY_LENGTH];
blake3_hasher hasher;
memcpy(material, key, key_len);
memcpy(material + key_len, salt, salt_len);
blake3_hasher_init_derive_key(&hasher, SS2022_SUBKEY_CTX);
blake3_hasher_update(&hasher, material, key_len + salt_len);
blake3_hasher_finalize(&hasher, subkey, subkey_len);
sodium_memzero(material, sizeof(material));
}
/*
* Server-side salt pool. SIP022 requires exact salt matching with 60s of
* retention and forbids bloom filters (false positives are not allowed),
* so ppbloom cannot be reused here.
*/
static struct cache *ss2022_salt_pool = NULL;
static int
ss2022_salt_check_and_add(const uint8_t *salt, size_t salt_len)
{
if (ss2022_salt_pool == NULL) {
if (cache_create(&ss2022_salt_pool, SS2022_SALT_POOL_CAP, NULL) != 0)
return CRYPTO_ERROR;
}
cache_clear(ss2022_salt_pool, SS2022_SALT_RETENTION);
if (cache_key_exist(ss2022_salt_pool, (char *)salt, salt_len)) {
LOGE("crypto: AEAD-2022: repeat salt detected");
return CRYPTO_ERROR;
}
cache_insert(ss2022_salt_pool, (char *)salt, salt_len, NULL);
return CRYPTO_OK;
}
static int
ss2022_check_timestamp(uint64_t ts)
{
uint64_t now = (uint64_t)time(NULL);
uint64_t diff = now > ts ? now - ts : ts - now;
return diff <= SS2022_TIME_WINDOW ? CRYPTO_OK : CRYPTO_ERROR;
}
static void
store16_be(uint8_t *p, uint16_t v)
{
p[0] = (uint8_t)(v >> 8);
p[1] = (uint8_t)v;
}
static void
store64_be(uint8_t *p, uint64_t v)
{
for (int i = 7; i >= 0; i--) {
p[i] = (uint8_t)(v & 0xff);
v >>= 8;
}
}
static uint64_t
load64_be(const uint8_t *p)
{
uint64_t v = 0;
for (int i = 0; i < 8; i++)
v = (v << 8) | p[i];
return v;
}
/*
* Length of the SOCKS5 address (ATYP + address + port) at the head of buf,
* or 0 if it is malformed or truncated.
*/
static size_t
ss2022_socks_addr_len(const uint8_t *buf, size_t len)
{
if (len < 1)
return 0;
size_t addr_len;
switch (buf[0] & ADDRTYPE_MASK) {
case 1: /* IPv4 */
addr_len = 1 + 4;
break;
case 4: /* IPv6 */
addr_len = 1 + 16;
break;
case 3: /* domain */
if (len < 2)
return 0;
addr_len = 2 + buf[1];
break;
default:
return 0;
}
return (len >= addr_len + 2) ? addr_len + 2 : 0;
}
/* Encrypt one chunk at c, then advance the counting nonce. */
static int
ss2022_seal(cipher_ctx_t *ctx, uint8_t *c, const uint8_t *p, size_t plen)
{
size_t clen = plen + SS2022_TAG_LEN;
int err = aead_cipher_encrypt(ctx, c, &clen, (uint8_t *)p, plen,
NULL, 0, ctx->nonce, ctx->skey);
if (err)
return -1;
sodium_increment(ctx->nonce, ctx->cipher->nonce_len);
return (int)clen;
}
/* Decrypt one chunk (mlen includes the tag), then advance the nonce. */
static int
ss2022_open(cipher_ctx_t *ctx, uint8_t *p, const uint8_t *c, size_t mlen)
{
size_t plen = mlen - SS2022_TAG_LEN;
int err = aead_cipher_decrypt(ctx, p, &plen, (uint8_t *)c, mlen,
NULL, 0, ctx->nonce, ctx->skey);
if (err)
return -1;
sodium_increment(ctx->nonce, ctx->cipher->nonce_len);
return (int)plen;
}
/* ------------------------------------------------------------------ TCP */
/*
* Build the request variable-length header body:
* ATYP || address || port || padding length || padding || initial payload
* The caller's first plaintext holds the SOCKS address, optionally followed
* by initial payload bytes. SIP022 requires padding when there is no initial
* payload, so random padding is added in that case.
*/
static int
ss2022_build_req_varhdr(const uint8_t *first, size_t first_len,
uint8_t **out, size_t *out_len)
{
size_t addr_len = ss2022_socks_addr_len(first, first_len);
if (addr_len == 0) {
LOGE("AEAD-2022: malformed SOCKS address in request header");
return CRYPTO_ERROR;
}
size_t payload_len = first_len - addr_len;
uint16_t pad_len = 0;
if (payload_len == 0) {
uint16_t r;
rand_bytes(&r, sizeof(r));
pad_len = (uint16_t)(r % SS2022_MAX_PADDING) + 1;
}
size_t total = addr_len + 2 + pad_len + payload_len;
uint8_t *buf = ss_malloc(total);
memcpy(buf, first, addr_len);
store16_be(buf + addr_len, pad_len);
if (pad_len > 0)
rand_bytes(buf + addr_len + 2, pad_len);
if (payload_len > 0)
memcpy(buf + addr_len + 2 + pad_len, first + addr_len, payload_len);
*out = buf;
*out_len = total;
return CRYPTO_OK;
}
/*
* Emit salt and header chunks for the first write of a stream. The salt and
* headers are written into one buffer so the caller sends them in a single
* write, as SIP022 requires.
*/
static int
ss2022_emit_header(cipher_ctx_t *ctx, buffer_t *out, size_t *out_len,
const uint8_t *first, size_t first_len, size_t capacity)
{
size_t salt_len = ctx->cipher->key_len;
uint64_t now = (uint64_t)time(NULL);
uint8_t fixed[SS2022_FIXED_BUF_LEN];
size_t fixed_len;
uint8_t *body = NULL;
size_t body_len = 0;
int body_owned = 0;
int rc = CRYPTO_ERROR;
if (ctx->role == CRYPTO_ROLE_CLIENT) {
if (ss2022_build_req_varhdr(first, first_len, &body, &body_len)
!= CRYPTO_OK)
return CRYPTO_ERROR;
body_owned = 1;
if (body_len > SS2022_CHUNK_MASK) {
LOGE("AEAD-2022: request header too large");
goto done;
}
fixed_len = SS2022_REQ_FIXED_LEN;
fixed[0] = SS2022_HDR_TYPE_CLIENT;
store64_be(fixed + 1, now);
store16_be(fixed + 9, (uint16_t)body_len);
} else {
/*
* The response fixed header doubles as the first length chunk and
* echoes the request salt, which lives in the paired decrypt ctx.
*/
cipher_ctx_t *peer = ctx->peer;
if (peer == NULL || !peer->init) {
LOGE("AEAD-2022: response header needs the request salt");
return CRYPTO_ERROR;
}
if (first_len > SS2022_CHUNK_MASK) {
LOGE("AEAD-2022: response payload too large");
return CRYPTO_ERROR;
}
body = (uint8_t *)first;
body_len = first_len;
fixed_len = SS2022_RESP_FIXED_LEN(salt_len);
fixed[0] = SS2022_HDR_TYPE_SERVER;
store64_be(fixed + 1, now);
memcpy(fixed + 9, peer->salt, salt_len);
store16_be(fixed + 9 + salt_len, (uint16_t)body_len);
}
brealloc(out, salt_len + fixed_len + SS2022_TAG_LEN
+ body_len + SS2022_TAG_LEN, capacity);
memcpy(out->data, ctx->salt, salt_len);
size_t o = salt_len;
int n = ss2022_seal(ctx, (uint8_t *)out->data + o, fixed, fixed_len);
if (n < 0)
goto done;
o += n;
n = ss2022_seal(ctx, (uint8_t *)out->data + o, body, body_len);
if (n < 0)
goto done;
o += n;
*out_len = o;
rc = CRYPTO_OK;
done:
if (body_owned)
ss_free(body);
return rc;
}
int
ss2022_tcp_encrypt(buffer_t *plaintext, cipher_ctx_t *ctx, size_t capacity)
{
static buffer_t tmp = { 0, 0, 0, NULL };
if (plaintext->len == 0)
return CRYPTO_OK;
if (!ctx->init) {
aead_cipher_ctx_set_key(ctx, 1);
ctx->init = 1;
size_t out_len = 0;
int err = ss2022_emit_header(ctx, &tmp, &out_len,
(uint8_t *)plaintext->data,
plaintext->len, capacity);
if (err)
return err;
tmp.len = out_len;
bswap_data(plaintext, &tmp);
plaintext->len = out_len;
return CRYPTO_OK;
}
/* Steady state: length chunk followed by payload chunk. */
size_t remaining = plaintext->len;
uint8_t *src = (uint8_t *)plaintext->data;
size_t total_out = 0;
size_t nchunks = (plaintext->len + SS2022_CHUNK_MASK - 1) / SS2022_CHUNK_MASK;
brealloc(&tmp, plaintext->len
+ nchunks * (SS2022_CHUNK_LEN_SIZE + 2 * SS2022_TAG_LEN),
capacity);
while (remaining > 0) {
uint16_t plen = remaining > SS2022_CHUNK_MASK
? SS2022_CHUNK_MASK : (uint16_t)remaining;
uint8_t len_buf[SS2022_CHUNK_LEN_SIZE];
store16_be(len_buf, plen);
int n = ss2022_seal(ctx, (uint8_t *)tmp.data + total_out,
len_buf, SS2022_CHUNK_LEN_SIZE);
if (n < 0)
return CRYPTO_ERROR;
total_out += n;
n = ss2022_seal(ctx, (uint8_t *)tmp.data + total_out, src, plen);
if (n < 0)
return CRYPTO_ERROR;
total_out += n;
src += plen;
remaining -= plen;
}
tmp.len = total_out;
bswap_data(plaintext, &tmp);
plaintext->len = total_out;
return CRYPTO_OK;
}
/*
* Parse a decrypted request variable-length header and emit the SOCKS
* address followed by any initial payload, which is the layout the
* ss-server request path already understands.
*/
static int
ss2022_parse_req_varhdr(const uint8_t *h, size_t len, buffer_t *out,
size_t capacity)
{
size_t socks_len = ss2022_socks_addr_len(h, len);
if (socks_len == 0 || len < socks_len + 2)
return CRYPTO_ERROR;
uint16_t pad_len = load16_be(h + socks_len);
size_t pad_ofst = socks_len + 2;
if (pad_len > SS2022_MAX_PADDING || pad_ofst + pad_len > len)
return CRYPTO_ERROR;
size_t payload_ofst = pad_ofst + pad_len;
size_t payload_len = len - payload_ofst;
/* SIP022: a request with neither payload nor padding must be rejected. */
if (payload_len == 0 && pad_len == 0)
return CRYPTO_ERROR;
brealloc(out, socks_len + payload_len, capacity);
memcpy(out->data, h, socks_len);
if (payload_len > 0)
memcpy(out->data + socks_len, h + payload_ofst, payload_len);
out->len = socks_len + payload_len;
return CRYPTO_OK;
}
/* Append n bytes of freshly decrypted plaintext to the output buffer. */
static int
ss2022_drain_chunks(cipher_ctx_t *ctx, buffer_t *chunk, buffer_t *out,
size_t capacity)
{
for (;;) {
if (ctx->next_plen == 0) {
/* Need a length chunk first. */
if (chunk->len < SS2022_CHUNK_LEN_SIZE + SS2022_TAG_LEN)
return CRYPTO_OK;
uint8_t len_buf[SS2022_CHUNK_LEN_SIZE];
int n = ss2022_open(ctx, len_buf,
(uint8_t *)chunk->data + chunk->idx,
SS2022_CHUNK_LEN_SIZE + SS2022_TAG_LEN);
if (n < 0) {
LOGE("AEAD-2022: length chunk authentication failed");
return CRYPTO_ERROR;
}
uint16_t plen = load16_be(len_buf);
if (plen == 0) {
LOGE("AEAD-2022: zero-length payload chunk");
return CRYPTO_ERROR;
}
chunk->idx += SS2022_CHUNK_LEN_SIZE + SS2022_TAG_LEN;
chunk->len -= SS2022_CHUNK_LEN_SIZE + SS2022_TAG_LEN;
ctx->next_plen = plen;
}
/*
* The length chunk's nonce has already been consumed, so the pending
* length is kept in ctx->next_plen until the payload arrives.
*/
size_t need = (size_t)ctx->next_plen + SS2022_TAG_LEN;
if (chunk->len < need)
return CRYPTO_OK;
brealloc(out, out->len + ctx->next_plen, capacity);
int n = ss2022_open(ctx, (uint8_t *)out->data + out->len,
(uint8_t *)chunk->data + chunk->idx, need);
if (n < 0) {
LOGE("AEAD-2022: payload chunk authentication failed");
return CRYPTO_ERROR;
}
chunk->idx += need;
chunk->len -= need;
out->len += n;
ctx->next_plen = 0;
}
}
int
ss2022_tcp_decrypt(buffer_t *ciphertext, cipher_ctx_t *ctx, size_t capacity)
{
static buffer_t out = { 0, 0, 0, NULL };
size_t salt_len = ctx->cipher->key_len;
int is_client = (ctx->role == CRYPTO_ROLE_CLIENT);
if (ctx->chunk == NULL) {
ctx->chunk = (buffer_t *)ss_malloc(sizeof(buffer_t));
memset(ctx->chunk, 0, sizeof(buffer_t));
balloc(ctx->chunk, capacity);
}
buffer_t *chunk = ctx->chunk;
/* Accumulate the newly received ciphertext. */
if (chunk->idx > 0) {
if (chunk->len > 0)
memmove(chunk->data, chunk->data + chunk->idx, chunk->len);
chunk->idx = 0;
}
brealloc(chunk, chunk->len + ciphertext->len, capacity);
memcpy(chunk->data + chunk->len, ciphertext->data, ciphertext->len);
chunk->len += ciphertext->len;
out.len = 0;
/* Stage 0: salt and fixed-length header, consumed as one unit. */
if (!ctx->init) {
size_t fixed_len = is_client ? SS2022_RESP_FIXED_LEN(salt_len)
: SS2022_REQ_FIXED_LEN;
size_t need = salt_len + fixed_len + SS2022_TAG_LEN;
if (chunk->len < need) {
ciphertext->len = 0;
return CRYPTO_NEED_MORE;
}
memcpy(ctx->salt, chunk->data + chunk->idx, salt_len);
if (!is_client
&& ss2022_salt_check_and_add(ctx->salt, salt_len) != CRYPTO_OK)
return CRYPTO_ERROR;
aead_cipher_ctx_set_key(ctx, 0);
uint8_t fixed[SS2022_FIXED_BUF_LEN];
int n = ss2022_open(ctx, fixed,
(uint8_t *)chunk->data + chunk->idx + salt_len,
fixed_len + SS2022_TAG_LEN);
if (n < 0) {
LOGE("AEAD-2022: fixed header authentication failed");
return CRYPTO_ERROR;
}
uint8_t want_type = is_client ? SS2022_HDR_TYPE_SERVER
: SS2022_HDR_TYPE_CLIENT;
if (fixed[0] != want_type) {
LOGE("AEAD-2022: unexpected header type %u", fixed[0]);
return CRYPTO_ERROR;
}
if (ss2022_check_timestamp(load64_be(fixed + 1)) != CRYPTO_OK) {
LOGE("AEAD-2022: header timestamp out of window");
return CRYPTO_ERROR;
}
if (is_client) {
cipher_ctx_t *peer = ctx->peer;
if (peer == NULL || memcmp(fixed + 9, peer->salt, salt_len) != 0) {
LOGE("AEAD-2022: response does not echo the request salt");
return CRYPTO_ERROR;
}
ctx->next_plen = load16_be(fixed + 9 + salt_len);
} else {
ctx->hdr_varlen = load16_be(fixed + 9);
}
chunk->idx += need;
chunk->len -= need;
ctx->init = 1;
ctx->hdr_stage = SS2022_STAGE_VAR_HDR;
}
/*
* Stage 1: the request's variable-length header, or the response's first
* payload chunk whose length came from the fixed header.
*/
if (ctx->hdr_stage == SS2022_STAGE_VAR_HDR) {
uint16_t want = is_client ? ctx->next_plen : ctx->hdr_varlen;
size_t need = (size_t)want + SS2022_TAG_LEN;
if (want == 0) {
LOGE("AEAD-2022: empty header chunk");
return CRYPTO_ERROR;
}
if (chunk->len < need) {
ciphertext->len = 0;
return CRYPTO_NEED_MORE;
}
uint8_t *body = ss_malloc(want);
int n = ss2022_open(ctx, body,
(uint8_t *)chunk->data + chunk->idx, need);
if (n < 0) {
ss_free(body);
LOGE("AEAD-2022: header chunk authentication failed");
return CRYPTO_ERROR;
}
chunk->idx += need;
chunk->len -= need;
int rc = CRYPTO_OK;
if (is_client) {
brealloc(&out, n, capacity);
memcpy(out.data, body, n);
out.len = n;
} else {
rc = ss2022_parse_req_varhdr(body, n, &out, capacity);
if (rc != CRYPTO_OK)
LOGE("AEAD-2022: malformed request header");
}
ss_free(body);
if (rc != CRYPTO_OK)
return rc;
ctx->next_plen = 0;
ctx->hdr_stage = SS2022_STAGE_STREAM;
}
/* Stage 2: length chunk and payload chunk, repeated. */
if (ss2022_drain_chunks(ctx, chunk, &out, capacity) != CRYPTO_OK)
return CRYPTO_ERROR;
if (chunk->len == 0)
chunk->idx = 0;
if (out.len == 0) {
ciphertext->len = 0;
return CRYPTO_NEED_MORE;
}
brealloc(ciphertext, out.len, capacity);
memcpy(ciphertext->data, out.data, out.len);
ciphertext->len = out.len;
return CRYPTO_OK;
}
+595
View File
@@ -0,0 +1,595 @@
/*
* aead2022_udp.c - Shadowsocks 2022 Edition (SIP022) session-based UDP
*
* Copyright (C) 2013 - 2026, Max Lv <max.c.lv@gmail.com>
*
* This file is part of the shadowsocks-libev.
*
* shadowsocks-libev is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* shadowsocks-libev is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with shadowsocks-libev; see the file COPYING. If not, see
* <http://www.gnu.org/licenses/>.
*
* A UDP packet is:
*
* AES methods: [AES-ECB(psk, session ID || packet ID)] [AEAD body]
* ChaCha: [24B random nonce] [XChaCha20-Poly1305(psk) body]
*
* The body carries a main header (type, timestamp, optional client session
* ID, padding, SOCKS address) followed by the payload.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <time.h>
#include <sodium.h>
#include <mbedtls/aes.h>
#include "aead.h"
#include "aead_internal.h"
#include "cache.h"
#include "utils.h"
#define SS2022_UDP_TYPE_CLIENT 0
#define SS2022_UDP_TYPE_SERVER 1
#define SS2022_UDP_SEP_HDR_LEN 16
#define SS2022_UDP_TAG_LEN 16
#define SS2022_UDP_XNONCE_LEN 24
/* type(1) + timestamp(8) + padding length(2) */
#define SS2022_UDP_MIN_HDR 11
/* sliding window width for replay protection */
#define SS2022_WINDOW_BITS 256
/*
* Per-conversation UDP session state. The client keeps one of these per
* source address; the server keeps one per client session ID. It holds our
* own outgoing session (ID, packet counter) and the replay window of the
* peer session we are receiving from.
*/
typedef struct ss2022_udp_session {
cipher_t *cipher;
/* outgoing */
uint8_t tx_session_id[SS2022_SESSION_ID_LEN];
uint64_t tx_packet_id;
uint8_t tx_subkey[MAX_KEY_LENGTH];
int tx_ready;
/* incoming: last accepted peer session and its sliding window */
uint8_t rx_session_id[SS2022_SESSION_ID_LEN];
uint8_t rx_subkey[MAX_KEY_LENGTH];
int rx_ready;
uint64_t rx_highest;
uint8_t rx_window[SS2022_WINDOW_BITS / 8];
/* peer's client session ID, echoed by the server in its replies */
uint8_t peer_session_id[SS2022_SESSION_ID_LEN];
int peer_session_known;
/*
* Server sessions are owned by the session table (they are looked up by
* client session ID, which is only known after decryption); client
* sessions are owned by the caller's slot.
*/
int table_owned;
} ss2022_udp_session_t;
/*
* Server-side session table, keyed by the 8-byte client session ID. SIP022
* requires servers to route by session ID rather than source address, so
* that sessions survive client network changes.
*/
#define SS2022_SESSION_TABLE_CAP 4096
#define SS2022_SESSION_TTL 600
static struct cache *ss2022_sessions = NULL;
static void
session_free_cb(void *key, void *element)
{
(void)key;
if (element != NULL) {
sodium_memzero(element, sizeof(ss2022_udp_session_t));
ss_free(element);
}
}
static void
store16_be(uint8_t *p, uint16_t v)
{
p[0] = (uint8_t)(v >> 8);
p[1] = (uint8_t)v;
}
static void
store64_be(uint8_t *p, uint64_t v)
{
for (int i = 7; i >= 0; i--) {
p[i] = (uint8_t)(v & 0xff);
v >>= 8;
}
}
static uint64_t
load64_be(const uint8_t *p)
{
uint64_t v = 0;
for (int i = 0; i < 8; i++)
v = (v << 8) | p[i];
return v;
}
static int
ss2022_check_timestamp(uint64_t ts)
{
uint64_t now = (uint64_t)time(NULL);
uint64_t diff = now > ts ? now - ts : ts - now;
return diff <= SS2022_TIME_WINDOW ? CRYPTO_OK : CRYPTO_ERROR;
}
/*
* Sliding window replay filter (WireGuard style). Only called after the
* packet has been authenticated and its header validated.
*/
static int
window_check(const ss2022_udp_session_t *s, uint64_t id)
{
if (id > s->rx_highest)
return CRYPTO_OK;
if (s->rx_highest - id >= SS2022_WINDOW_BITS)
return CRYPTO_ERROR;
uint64_t bit = id % SS2022_WINDOW_BITS;
return (s->rx_window[bit / 8] & (1u << (bit % 8)))
? CRYPTO_ERROR : CRYPTO_OK;
}
static void
window_update(ss2022_udp_session_t *s, uint64_t id)
{
if (id > s->rx_highest) {
uint64_t shift = id - s->rx_highest;
if (shift >= SS2022_WINDOW_BITS) {
memset(s->rx_window, 0, sizeof(s->rx_window));
} else {
for (uint64_t i = s->rx_highest + 1; i <= id; i++) {
uint64_t b = i % SS2022_WINDOW_BITS;
s->rx_window[b / 8] &= (uint8_t) ~(1u << (b % 8));
}
}
s->rx_highest = id;
}
uint64_t bit = id % SS2022_WINDOW_BITS;
s->rx_window[bit / 8] |= (uint8_t)(1u << (bit % 8));
}
static ss2022_udp_session_t *
session_new(cipher_t *cipher)
{
ss2022_udp_session_t *s = ss_malloc(sizeof(ss2022_udp_session_t));
memset(s, 0, sizeof(ss2022_udp_session_t));
s->cipher = cipher;
return s;
}
static ss2022_udp_session_t *
session_get(void **slot, cipher_t *cipher)
{
ss2022_udp_session_t *s = (ss2022_udp_session_t *)*slot;
if (s != NULL)
return s;
s = session_new(cipher);
*slot = s;
return s;
}
/* Look up (or create) the server-side session for a client session ID. */
static ss2022_udp_session_t *
session_by_id(const uint8_t *session_id, cipher_t *cipher)
{
if (ss2022_sessions == NULL
&& cache_create(&ss2022_sessions, SS2022_SESSION_TABLE_CAP,
session_free_cb) != 0)
return NULL;
ss2022_udp_session_t *s = NULL;
cache_lookup(ss2022_sessions, (char *)session_id, SS2022_SESSION_ID_LEN,
(void *)&s);
if (s != NULL)
return s;
cache_clear(ss2022_sessions, SS2022_SESSION_TTL);
s = session_new(cipher);
s->table_owned = 1;
cache_insert(ss2022_sessions, (char *)session_id, SS2022_SESSION_ID_LEN, s);
return s;
}
void
aead_2022_udp_session_release(void *slot)
{
ss2022_udp_session_t *s = (ss2022_udp_session_t *)slot;
if (s == NULL || s->table_owned)
return; /* table-owned sessions are freed when the table evicts them */
sodium_memzero(s, sizeof(ss2022_udp_session_t));
ss_free(s);
}
static int
is_chacha(const cipher_t *cipher)
{
return cipher->method == CHACHA20POLY1305IETF2022;
}
/* AES-ECB over the 16-byte separate header, keyed directly with the PSK. */
static int
sep_hdr_crypt(const cipher_t *cipher, uint8_t *dst, const uint8_t *src, int enc)
{
mbedtls_aes_context aes;
int err;
mbedtls_aes_init(&aes);
if (enc)
err = mbedtls_aes_setkey_enc(&aes, cipher->key,
(unsigned int)cipher->key_len * 8);
else
err = mbedtls_aes_setkey_dec(&aes, cipher->key,
(unsigned int)cipher->key_len * 8);
if (err == 0)
err = mbedtls_aes_crypt_ecb(&aes,
enc ? MBEDTLS_AES_ENCRYPT
: MBEDTLS_AES_DECRYPT,
src, dst);
mbedtls_aes_free(&aes);
return err == 0 ? CRYPTO_OK : CRYPTO_ERROR;
}
/*
* AEAD over the packet body. For AES methods the session subkey and a nonce
* taken from the separate header are used; for ChaCha the PSK is used
* directly with a random 24-byte nonce.
*/
static int
body_seal(const cipher_t *cipher, const uint8_t *key,
uint8_t *c, size_t *clen, const uint8_t *m, size_t mlen,
const uint8_t *nonce)
{
unsigned long long out_len = 0;
int err;
if (is_chacha(cipher)) {
err = crypto_aead_xchacha20poly1305_ietf_encrypt(
c, &out_len, m, mlen, NULL, 0, NULL, nonce, key);
*clen = (size_t)out_len;
return err == 0 ? CRYPTO_OK : CRYPTO_ERROR;
}
cipher_t tmp_cipher = *cipher;
cipher_ctx_t tmp_ctx;
memset(&tmp_ctx, 0, sizeof(tmp_ctx));
tmp_ctx.cipher = &tmp_cipher;
/* Reuse the shared one-shot helper with an mbedTLS GCM context. */
tmp_ctx.evp = ss_malloc(sizeof(cipher_evp_t));
memset(tmp_ctx.evp, 0, sizeof(cipher_evp_t));
mbedtls_cipher_init(tmp_ctx.evp);
const cipher_kt_t *kt = mbedtls_cipher_info_from_string(
cipher->key_len == 16 ? "AES-128-GCM" : "AES-256-GCM");
if (kt == NULL || mbedtls_cipher_setup(tmp_ctx.evp, kt) != 0
|| mbedtls_cipher_setkey(tmp_ctx.evp, key,
(int)cipher->key_len * 8, MBEDTLS_ENCRYPT) != 0) {
mbedtls_cipher_free(tmp_ctx.evp);
ss_free(tmp_ctx.evp);
return CRYPTO_ERROR;
}
*clen = mlen + SS2022_UDP_TAG_LEN;
err = aead_cipher_encrypt(&tmp_ctx, c, clen, (uint8_t *)m, mlen,
NULL, 0, (uint8_t *)nonce, (uint8_t *)key);
mbedtls_cipher_free(tmp_ctx.evp);
ss_free(tmp_ctx.evp);
return err == 0 ? CRYPTO_OK : CRYPTO_ERROR;
}
static int
body_open(const cipher_t *cipher, const uint8_t *key,
uint8_t *p, size_t *plen, const uint8_t *c, size_t clen,
const uint8_t *nonce)
{
unsigned long long out_len = 0;
int err;
if (is_chacha(cipher)) {
err = crypto_aead_xchacha20poly1305_ietf_decrypt(
p, &out_len, NULL, c, clen, NULL, 0, nonce, key);
*plen = (size_t)out_len;
return err == 0 ? CRYPTO_OK : CRYPTO_ERROR;
}
cipher_t tmp_cipher = *cipher;
cipher_ctx_t tmp_ctx;
memset(&tmp_ctx, 0, sizeof(tmp_ctx));
tmp_ctx.cipher = &tmp_cipher;
tmp_ctx.evp = ss_malloc(sizeof(cipher_evp_t));
memset(tmp_ctx.evp, 0, sizeof(cipher_evp_t));
mbedtls_cipher_init(tmp_ctx.evp);
const cipher_kt_t *kt = mbedtls_cipher_info_from_string(
cipher->key_len == 16 ? "AES-128-GCM" : "AES-256-GCM");
if (kt == NULL || mbedtls_cipher_setup(tmp_ctx.evp, kt) != 0
|| mbedtls_cipher_setkey(tmp_ctx.evp, key,
(int)cipher->key_len * 8, MBEDTLS_DECRYPT) != 0) {
mbedtls_cipher_free(tmp_ctx.evp);
ss_free(tmp_ctx.evp);
return CRYPTO_ERROR;
}
*plen = clen - SS2022_UDP_TAG_LEN;
err = aead_cipher_decrypt(&tmp_ctx, p, plen, (uint8_t *)c, clen,
NULL, 0, (uint8_t *)nonce, (uint8_t *)key);
mbedtls_cipher_free(tmp_ctx.evp);
ss_free(tmp_ctx.evp);
return err == 0 ? CRYPTO_OK : CRYPTO_ERROR;
}
/*
* Encrypt one UDP packet. The plaintext is a SOCKS address followed by the
* payload, matching what udprelay hands to the legacy encrypt_all.
*/
int
aead_2022_encrypt_udp(buffer_t *plaintext, cipher_t *cipher, size_t capacity,
void **slot)
{
static buffer_t tmp = { 0, 0, 0, NULL };
ss2022_udp_session_t *s = session_get(slot, cipher);
int is_server = (aead_get_role() == CRYPTO_ROLE_SERVER);
size_t salt_len = cipher->key_len;
if (!s->tx_ready) {
rand_bytes(s->tx_session_id, SS2022_SESSION_ID_LEN);
s->tx_packet_id = 0;
ss2022_derive_subkey(cipher->key, salt_len,
s->tx_session_id, SS2022_SESSION_ID_LEN,
s->tx_subkey, salt_len);
s->tx_ready = 1;
}
/* Build the main header followed by the payload. */
size_t extra = is_server ? SS2022_SESSION_ID_LEN : 0;
size_t hdr_len = SS2022_UDP_MIN_HDR + extra;
size_t body_len = hdr_len + plaintext->len;
uint8_t *body = ss_malloc(body_len);
size_t o = 0;
body[o++] = is_server ? SS2022_UDP_TYPE_SERVER : SS2022_UDP_TYPE_CLIENT;
store64_be(body + o, (uint64_t)time(NULL));
o += 8;
if (is_server) {
if (!s->peer_session_known) {
ss_free(body);
LOGE("AEAD-2022: no client session to reply to");
return CRYPTO_ERROR;
}
memcpy(body + o, s->peer_session_id, SS2022_SESSION_ID_LEN);
o += SS2022_SESSION_ID_LEN;
}
store16_be(body + o, 0); /* padding length */
o += 2;
memcpy(body + o, plaintext->data, plaintext->len);
int rc = CRYPTO_ERROR;
if (is_chacha(cipher)) {
/*
* nonce || XChaCha20-Poly1305(psk, session ID || packet ID ||
* main header || payload)
*/
size_t merged_len = SS2022_UDP_SEP_HDR_LEN + body_len;
uint8_t *merged = ss_malloc(merged_len);
memcpy(merged, s->tx_session_id, SS2022_SESSION_ID_LEN);
store64_be(merged + SS2022_SESSION_ID_LEN, s->tx_packet_id);
memcpy(merged + SS2022_UDP_SEP_HDR_LEN, body, body_len);
brealloc(&tmp, SS2022_UDP_XNONCE_LEN + merged_len + SS2022_UDP_TAG_LEN,
capacity);
rand_bytes(tmp.data, SS2022_UDP_XNONCE_LEN);
size_t clen = 0;
rc = body_seal(cipher, cipher->key,
(uint8_t *)tmp.data + SS2022_UDP_XNONCE_LEN, &clen,
merged, merged_len, (uint8_t *)tmp.data);
sodium_memzero(merged, merged_len);
ss_free(merged);
if (rc == CRYPTO_OK)
tmp.len = SS2022_UDP_XNONCE_LEN + clen;
} else {
uint8_t sep[SS2022_UDP_SEP_HDR_LEN];
memcpy(sep, s->tx_session_id, SS2022_SESSION_ID_LEN);
store64_be(sep + SS2022_SESSION_ID_LEN, s->tx_packet_id);
brealloc(&tmp, SS2022_UDP_SEP_HDR_LEN + body_len + SS2022_UDP_TAG_LEN,
capacity);
rc = sep_hdr_crypt(cipher, (uint8_t *)tmp.data, sep, 1);
if (rc == CRYPTO_OK) {
size_t clen = 0;
/* nonce is separate_header[4..16] of the plaintext header */
rc = body_seal(cipher, s->tx_subkey,
(uint8_t *)tmp.data + SS2022_UDP_SEP_HDR_LEN, &clen,
body, body_len, sep + 4);
if (rc == CRYPTO_OK)
tmp.len = SS2022_UDP_SEP_HDR_LEN + clen;
}
}
sodium_memzero(body, body_len);
ss_free(body);
if (rc != CRYPTO_OK)
return CRYPTO_ERROR;
s->tx_packet_id++;
bswap_data(plaintext, &tmp);
plaintext->len = tmp.len;
return CRYPTO_OK;
}
int
aead_2022_decrypt_udp(buffer_t *ciphertext, cipher_t *cipher, size_t capacity,
void **slot)
{
static buffer_t tmp = { 0, 0, 0, NULL };
int is_server = (aead_get_role() == CRYPTO_ROLE_SERVER);
size_t salt_len = cipher->key_len;
uint8_t session_id[SS2022_SESSION_ID_LEN];
uint64_t packet_id;
size_t plen = 0;
if (is_chacha(cipher)) {
if (ciphertext->len <= SS2022_UDP_XNONCE_LEN + SS2022_UDP_SEP_HDR_LEN
+ SS2022_UDP_TAG_LEN)
return CRYPTO_ERROR;
size_t clen = ciphertext->len - SS2022_UDP_XNONCE_LEN;
brealloc(&tmp, clen, capacity);
if (body_open(cipher, cipher->key, (uint8_t *)tmp.data, &plen,
(uint8_t *)ciphertext->data + SS2022_UDP_XNONCE_LEN, clen,
(uint8_t *)ciphertext->data) != CRYPTO_OK)
return CRYPTO_ERROR;
if (plen < SS2022_UDP_SEP_HDR_LEN)
return CRYPTO_ERROR;
memcpy(session_id, tmp.data, SS2022_SESSION_ID_LEN);
packet_id = load64_be((uint8_t *)tmp.data + SS2022_SESSION_ID_LEN);
/* strip the merged session ID and packet ID */
plen -= SS2022_UDP_SEP_HDR_LEN;
memmove(tmp.data, tmp.data + SS2022_UDP_SEP_HDR_LEN, plen);
} else {
if (ciphertext->len <= SS2022_UDP_SEP_HDR_LEN + SS2022_UDP_TAG_LEN)
return CRYPTO_ERROR;
uint8_t sep[SS2022_UDP_SEP_HDR_LEN];
if (sep_hdr_crypt(cipher, sep, (uint8_t *)ciphertext->data, 0)
!= CRYPTO_OK)
return CRYPTO_ERROR;
memcpy(session_id, sep, SS2022_SESSION_ID_LEN);
packet_id = load64_be(sep + SS2022_SESSION_ID_LEN);
uint8_t subkey[MAX_KEY_LENGTH];
ss2022_derive_subkey(cipher->key, salt_len,
session_id, SS2022_SESSION_ID_LEN,
subkey, salt_len);
size_t clen = ciphertext->len - SS2022_UDP_SEP_HDR_LEN;
brealloc(&tmp, clen, capacity);
int rc = body_open(cipher, subkey, (uint8_t *)tmp.data, &plen,
(uint8_t *)ciphertext->data + SS2022_UDP_SEP_HDR_LEN,
clen, sep + 4);
sodium_memzero(subkey, sizeof(subkey));
if (rc != CRYPTO_OK)
return CRYPTO_ERROR;
}
/*
* Resolve the session. The server routes by client session ID, which is
* only known now; the client keeps one session per source address, held
* in the caller's slot.
*/
ss2022_udp_session_t *s;
if (is_server) {
s = session_by_id(session_id, cipher);
if (s == NULL)
return CRYPTO_ERROR;
*slot = s; /* the reply path encrypts with this same session */
} else {
s = session_get(slot, cipher);
}
/* Main header validation */
size_t extra = is_server ? 0 : SS2022_SESSION_ID_LEN;
size_t hdr_len = SS2022_UDP_MIN_HDR + extra;
if (plen < hdr_len)
return CRYPTO_ERROR;
uint8_t *body = (uint8_t *)tmp.data;
uint8_t want_type = is_server ? SS2022_UDP_TYPE_CLIENT
: SS2022_UDP_TYPE_SERVER;
if (body[0] != want_type)
return CRYPTO_ERROR;
if (ss2022_check_timestamp(load64_be(body + 1)) != CRYPTO_OK)
return CRYPTO_ERROR;
size_t o = 9;
if (!is_server) {
/* the server echoes our client session ID; verify it is ours */
if (!s->tx_ready
|| memcmp(body + o, s->tx_session_id, SS2022_SESSION_ID_LEN) != 0)
return CRYPTO_ERROR;
o += SS2022_SESSION_ID_LEN;
}
uint16_t pad_len = load16_be(body + o);
o += 2;
if (o + pad_len > plen)
return CRYPTO_ERROR;
o += pad_len;
/* Replay protection: new session resets the window. */
if (!s->rx_ready
|| memcmp(s->rx_session_id, session_id, SS2022_SESSION_ID_LEN) != 0) {
memcpy(s->rx_session_id, session_id, SS2022_SESSION_ID_LEN);
memset(s->rx_window, 0, sizeof(s->rx_window));
s->rx_highest = 0;
s->rx_ready = 1;
/* packet ID 0 is legitimate for a fresh session */
if (packet_id > 0)
s->rx_highest = packet_id - 1;
} else if (window_check(s, packet_id) != CRYPTO_OK) {
LOGE("AEAD-2022: UDP replay detected");
return CRYPTO_ERROR;
}
window_update(s, packet_id);
if (is_server) {
memcpy(s->peer_session_id, session_id, SS2022_SESSION_ID_LEN);
s->peer_session_known = 1;
}
size_t payload_len = plen - o;
brealloc(ciphertext, payload_len ? payload_len : 1, capacity);
memcpy(ciphertext->data, body + o, payload_len);
ciphertext->len = payload_len;
return CRYPTO_OK;
}
+76
View File
@@ -0,0 +1,76 @@
/*
* aead_internal.h - Internal interface shared by aead.c and aead2022.c
*
* Copyright (C) 2013 - 2026, Max Lv <max.c.lv@gmail.com>
*
* This file is part of the shadowsocks-libev.
*
* shadowsocks-libev is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* shadowsocks-libev is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with shadowsocks-libev; see the file COPYING. If not, see
* <http://www.gnu.org/licenses/>.
*/
#ifndef _AEAD_INTERNAL_H
#define _AEAD_INTERNAL_H
#include "crypto.h"
/* AEAD method identifiers (indexes into the supported_aead_ciphers tables) */
#define AES128GCM 0
#define AES192GCM 1
#define AES256GCM 2
#define AES128GCM2022 3
#define AES256GCM2022 4
/*
* methods above require a gcm context
* methods below don't require it,
* then we need to fake one
*/
#define CHACHA20POLY1305IETF 5
#define CHACHA20POLY1305IETF2022 6
#ifdef FS_HAVE_XCHACHA20IETF
#define XCHACHA20POLY1305IETF 7
#endif
static inline int
is_aead_2022(int method)
{
return method == AES128GCM2022 || method == AES256GCM2022
|| method == CHACHA20POLY1305IETF2022;
}
/* Low-level one-shot AEAD operations implemented in aead.c */
int aead_cipher_encrypt(cipher_ctx_t *cipher_ctx,
uint8_t *c, size_t *clen,
uint8_t *m, size_t mlen,
uint8_t *ad, size_t adlen,
uint8_t *n, uint8_t *k);
int aead_cipher_decrypt(cipher_ctx_t *cipher_ctx,
uint8_t *p, size_t *plen,
uint8_t *m, size_t mlen,
uint8_t *ad, size_t adlen,
uint8_t *n, uint8_t *k);
void aead_cipher_ctx_set_key(cipher_ctx_t *cipher_ctx, int enc);
/* AEAD-2022 pieces implemented in aead2022.c */
int aead_get_role(void);
void ss2022_derive_subkey(const uint8_t *key, size_t key_len,
const uint8_t *salt, size_t salt_len,
uint8_t *subkey, size_t subkey_len);
int ss2022_tcp_encrypt(buffer_t *plaintext, cipher_ctx_t *cipher_ctx,
size_t capacity);
int ss2022_tcp_decrypt(buffer_t *ciphertext, cipher_ctx_t *cipher_ctx,
size_t capacity);
#endif // _AEAD_INTERNAL_H
+7 -1
View File
@@ -34,9 +34,15 @@
#include "base64.h"
/* ---------------- private code */
/*
* Decoding table indexed by (c - 43). It accepts both the standard alphabet
* ('+' and '/') and the URL-safe one ('-' and '_'), since keys are quoted
* from tools using either: `openssl rand -base64` emits standard base64,
* while shadowsocks configs have traditionally used the URL-safe form.
*/
static const uint8_t map2[] =
{
0xff, 0xff, 0x3e, 0xff, 0xff, 0x34, 0x35, 0x36,
0x3e, 0xff, 0x3e, 0xff, 0x3f, 0x34, 0x35, 0x36,
0x37, 0x38, 0x39, 0x3a, 0x3b, 0x3c, 0x3d, 0xff,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x01,
0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09,
+121
View File
@@ -0,0 +1,121 @@
Creative Commons Legal Code
CC0 1.0 Universal
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
HEREUNDER.
Statement of Purpose
The laws of most jurisdictions throughout the world automatically confer
exclusive Copyright and Related Rights (defined below) upon the creator
and subsequent owner(s) (each and all, an "owner") of an original work of
authorship and/or a database (each, a "Work").
Certain owners wish to permanently relinquish those rights to a Work for
the purpose of contributing to a commons of creative, cultural and
scientific works ("Commons") that the public can reliably and without fear
of later claims of infringement build upon, modify, incorporate in other
works, reuse and redistribute as freely as possible in any form whatsoever
and for any purposes, including without limitation commercial purposes.
These owners may contribute to the Commons to promote the ideal of a free
culture and the further production of creative, cultural and scientific
works, or to gain reputation or greater distribution for their Work in
part through the use and efforts of others.
For these and/or other purposes and motivations, and without any
expectation of additional consideration or compensation, the person
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
is an owner of Copyright and Related Rights in the Work, voluntarily
elects to apply CC0 to the Work and publicly distribute the Work under its
terms, with knowledge of his or her Copyright and Related Rights in the
Work and the meaning and intended legal effect of CC0 on those rights.
1. Copyright and Related Rights. A Work made available under CC0 may be
protected by copyright and related or neighboring rights ("Copyright and
Related Rights"). Copyright and Related Rights include, but are not
limited to, the following:
i. the right to reproduce, adapt, distribute, perform, display,
communicate, and translate a Work;
ii. moral rights retained by the original author(s) and/or performer(s);
iii. publicity and privacy rights pertaining to a person's image or
likeness depicted in a Work;
iv. rights protecting against unfair competition in regards to a Work,
subject to the limitations in paragraph 4(a), below;
v. rights protecting the extraction, dissemination, use and reuse of data
in a Work;
vi. database rights (such as those arising under Directive 96/9/EC of the
European Parliament and of the Council of 11 March 1996 on the legal
protection of databases, and under any national implementation
thereof, including any amended or successor version of such
directive); and
vii. other similar, equivalent or corresponding rights throughout the
world based on applicable law or treaty, and any national
implementations thereof.
2. Waiver. To the greatest extent permitted by, but not in contravention
of, applicable law, Affirmer hereby overtly, fully, permanently,
irrevocably and unconditionally waives, abandons, and surrenders all of
Affirmer's Copyright and Related Rights and associated claims and causes
of action, whether now known or unknown (including existing as well as
future claims and causes of action), in the Work (i) in all territories
worldwide, (ii) for the maximum duration provided by applicable law or
treaty (including future time extensions), (iii) in any current or future
medium and for any number of copies, and (iv) for any purpose whatsoever,
including without limitation commercial, advertising or promotional
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
member of the public at large and to the detriment of Affirmer's heirs and
successors, fully intending that such Waiver shall not be subject to
revocation, rescission, cancellation, termination, or any other legal or
equitable action to disrupt the quiet enjoyment of the Work by the public
as contemplated by Affirmer's express Statement of Purpose.
3. Public License Fallback. Should any part of the Waiver for any reason
be judged legally invalid or ineffective under applicable law, then the
Waiver shall be preserved to the maximum extent permitted taking into
account Affirmer's express Statement of Purpose. In addition, to the
extent the Waiver is so judged Affirmer hereby grants to each affected
person a royalty-free, non transferable, non sublicensable, non exclusive,
irrevocable and unconditional license to exercise Affirmer's Copyright and
Related Rights in the Work (i) in all territories worldwide, (ii) for the
maximum duration provided by applicable law or treaty (including future
time extensions), (iii) in any current or future medium and for any number
of copies, and (iv) for any purpose whatsoever, including without
limitation commercial, advertising or promotional purposes (the
"License"). The License shall be deemed effective as of the date CC0 was
applied by Affirmer to the Work. Should any part of the License for any
reason be judged legally invalid or ineffective under applicable law, such
partial invalidity or ineffectiveness shall not invalidate the remainder
of the License, and in such case Affirmer hereby affirms that he or she
will not (i) exercise any of his or her remaining Copyright and Related
Rights in the Work or (ii) assert any associated claims and causes of
action with respect to the Work, in either case contrary to Affirmer's
express Statement of Purpose.
4. Limitations and Disclaimers.
a. No trademark or patent rights held by Affirmer are waived, abandoned,
surrendered, licensed or otherwise affected by this document.
b. Affirmer offers the Work as-is and makes no representations or
warranties of any kind concerning the Work, express, implied,
statutory or otherwise, including without limitation warranties of
title, merchantability, fitness for a particular purpose, non
infringement, or the absence of latent or other defects, accuracy, or
the present or absence of errors, whether or not discoverable, all to
the greatest extent permissible under applicable law.
c. Affirmer disclaims responsibility for clearing rights of other persons
that may apply to the Work or any use thereof, including without
limitation any person's Copyright and Related Rights in the Work.
Further, Affirmer disclaims responsibility for obtaining any necessary
consents, permissions or other rights required for any use of the
Work.
d. Affirmer understands and acknowledges that Creative Commons is not a
party to this document and has no duty or obligation with respect to
this CC0 or use of the Work.
+623
View File
@@ -0,0 +1,623 @@
#include <assert.h>
#include <stdbool.h>
#include <string.h>
#include "blake3.h"
#include "blake3_impl.h"
const char *blake3_version(void) { return BLAKE3_VERSION_STRING; }
INLINE void chunk_state_init(blake3_chunk_state *self, const uint32_t key[8],
uint8_t flags) {
memcpy(self->cv, key, BLAKE3_KEY_LEN);
self->chunk_counter = 0;
memset(self->buf, 0, BLAKE3_BLOCK_LEN);
self->buf_len = 0;
self->blocks_compressed = 0;
self->flags = flags;
}
INLINE void chunk_state_reset(blake3_chunk_state *self, const uint32_t key[8],
uint64_t chunk_counter) {
memcpy(self->cv, key, BLAKE3_KEY_LEN);
self->chunk_counter = chunk_counter;
self->blocks_compressed = 0;
memset(self->buf, 0, BLAKE3_BLOCK_LEN);
self->buf_len = 0;
}
INLINE size_t chunk_state_len(const blake3_chunk_state *self) {
return (BLAKE3_BLOCK_LEN * (size_t)self->blocks_compressed) +
((size_t)self->buf_len);
}
INLINE size_t chunk_state_fill_buf(blake3_chunk_state *self,
const uint8_t *input, size_t input_len) {
size_t take = BLAKE3_BLOCK_LEN - ((size_t)self->buf_len);
if (take > input_len) {
take = input_len;
}
uint8_t *dest = self->buf + ((size_t)self->buf_len);
memcpy(dest, input, take);
self->buf_len += (uint8_t)take;
return take;
}
INLINE uint8_t chunk_state_maybe_start_flag(const blake3_chunk_state *self) {
if (self->blocks_compressed == 0) {
return CHUNK_START;
} else {
return 0;
}
}
typedef struct {
uint32_t input_cv[8];
uint64_t counter;
uint8_t block[BLAKE3_BLOCK_LEN];
uint8_t block_len;
uint8_t flags;
} output_t;
INLINE output_t make_output(const uint32_t input_cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter,
uint8_t flags) {
output_t ret;
memcpy(ret.input_cv, input_cv, 32);
memcpy(ret.block, block, BLAKE3_BLOCK_LEN);
ret.block_len = block_len;
ret.counter = counter;
ret.flags = flags;
return ret;
}
// Chaining values within a given chunk (specifically the compress_in_place
// interface) are represented as words. This avoids unnecessary bytes<->words
// conversion overhead in the portable implementation. However, the hash_many
// interface handles both user input and parent node blocks, so it accepts
// bytes. For that reason, chaining values in the CV stack are represented as
// bytes.
INLINE void output_chaining_value(const output_t *self, uint8_t cv[32]) {
uint32_t cv_words[8];
memcpy(cv_words, self->input_cv, 32);
blake3_compress_in_place(cv_words, self->block, self->block_len,
self->counter, self->flags);
store_cv_words(cv, cv_words);
}
INLINE void output_root_bytes(const output_t *self, uint64_t seek, uint8_t *out,
size_t out_len) {
if (out_len == 0) {
return;
}
uint64_t output_block_counter = seek / 64;
size_t offset_within_block = seek % 64;
uint8_t wide_buf[64];
if(offset_within_block) {
blake3_compress_xof(self->input_cv, self->block, self->block_len, output_block_counter, self->flags | ROOT, wide_buf);
const size_t available_bytes = 64 - offset_within_block;
const size_t bytes = out_len > available_bytes ? available_bytes : out_len;
memcpy(out, wide_buf + offset_within_block, bytes);
out += bytes;
out_len -= bytes;
output_block_counter += 1;
}
if(out_len / 64) {
blake3_xof_many(self->input_cv, self->block, self->block_len, output_block_counter, self->flags | ROOT, out, out_len / 64);
}
output_block_counter += out_len / 64;
out += out_len & -64;
out_len -= out_len & -64;
if(out_len) {
blake3_compress_xof(self->input_cv, self->block, self->block_len, output_block_counter, self->flags | ROOT, wide_buf);
memcpy(out, wide_buf, out_len);
}
}
INLINE void chunk_state_update(blake3_chunk_state *self, const uint8_t *input,
size_t input_len) {
if (self->buf_len > 0) {
size_t take = chunk_state_fill_buf(self, input, input_len);
input += take;
input_len -= take;
if (input_len > 0) {
blake3_compress_in_place(
self->cv, self->buf, BLAKE3_BLOCK_LEN, self->chunk_counter,
self->flags | chunk_state_maybe_start_flag(self));
self->blocks_compressed += 1;
self->buf_len = 0;
memset(self->buf, 0, BLAKE3_BLOCK_LEN);
}
}
while (input_len > BLAKE3_BLOCK_LEN) {
blake3_compress_in_place(self->cv, input, BLAKE3_BLOCK_LEN,
self->chunk_counter,
self->flags | chunk_state_maybe_start_flag(self));
self->blocks_compressed += 1;
input += BLAKE3_BLOCK_LEN;
input_len -= BLAKE3_BLOCK_LEN;
}
chunk_state_fill_buf(self, input, input_len);
}
INLINE output_t chunk_state_output(const blake3_chunk_state *self) {
uint8_t block_flags =
self->flags | chunk_state_maybe_start_flag(self) | CHUNK_END;
return make_output(self->cv, self->buf, self->buf_len, self->chunk_counter,
block_flags);
}
INLINE output_t parent_output(const uint8_t block[BLAKE3_BLOCK_LEN],
const uint32_t key[8], uint8_t flags) {
return make_output(key, block, BLAKE3_BLOCK_LEN, 0, flags | PARENT);
}
// Given some input larger than one chunk, return the number of bytes that
// should go in the left subtree. This is the largest power-of-2 number of
// chunks that leaves at least 1 byte for the right subtree.
INLINE size_t left_len(size_t content_len) {
// Subtract 1 to reserve at least one byte for the right side. content_len
// should always be greater than BLAKE3_CHUNK_LEN.
size_t full_chunks = (content_len - 1) / BLAKE3_CHUNK_LEN;
return round_down_to_power_of_2(full_chunks) * BLAKE3_CHUNK_LEN;
}
// Use SIMD parallelism to hash up to MAX_SIMD_DEGREE chunks at the same time
// on a single thread. Write out the chunk chaining values and return the
// number of chunks hashed. These chunks are never the root and never empty;
// those cases use a different codepath.
INLINE size_t compress_chunks_parallel(const uint8_t *input, size_t input_len,
const uint32_t key[8],
uint64_t chunk_counter, uint8_t flags,
uint8_t *out) {
#if defined(BLAKE3_TESTING)
assert(0 < input_len);
assert(input_len <= MAX_SIMD_DEGREE * BLAKE3_CHUNK_LEN);
#endif
const uint8_t *chunks_array[MAX_SIMD_DEGREE];
size_t input_position = 0;
size_t chunks_array_len = 0;
while (input_len - input_position >= BLAKE3_CHUNK_LEN) {
chunks_array[chunks_array_len] = &input[input_position];
input_position += BLAKE3_CHUNK_LEN;
chunks_array_len += 1;
}
blake3_hash_many(chunks_array, chunks_array_len,
BLAKE3_CHUNK_LEN / BLAKE3_BLOCK_LEN, key, chunk_counter,
true, flags, CHUNK_START, CHUNK_END, out);
// Hash the remaining partial chunk, if there is one. Note that the empty
// chunk (meaning the empty message) is a different codepath.
if (input_len > input_position) {
uint64_t counter = chunk_counter + (uint64_t)chunks_array_len;
blake3_chunk_state chunk_state;
chunk_state_init(&chunk_state, key, flags);
chunk_state.chunk_counter = counter;
chunk_state_update(&chunk_state, &input[input_position],
input_len - input_position);
output_t output = chunk_state_output(&chunk_state);
output_chaining_value(&output, &out[chunks_array_len * BLAKE3_OUT_LEN]);
return chunks_array_len + 1;
} else {
return chunks_array_len;
}
}
// Use SIMD parallelism to hash up to MAX_SIMD_DEGREE parents at the same time
// on a single thread. Write out the parent chaining values and return the
// number of parents hashed. (If there's an odd input chaining value left over,
// return it as an additional output.) These parents are never the root and
// never empty; those cases use a different codepath.
INLINE size_t compress_parents_parallel(const uint8_t *child_chaining_values,
size_t num_chaining_values,
const uint32_t key[8], uint8_t flags,
uint8_t *out) {
#if defined(BLAKE3_TESTING)
assert(2 <= num_chaining_values);
assert(num_chaining_values <= 2 * MAX_SIMD_DEGREE_OR_2);
#endif
const uint8_t *parents_array[MAX_SIMD_DEGREE_OR_2];
size_t parents_array_len = 0;
while (num_chaining_values - (2 * parents_array_len) >= 2) {
parents_array[parents_array_len] =
&child_chaining_values[2 * parents_array_len * BLAKE3_OUT_LEN];
parents_array_len += 1;
}
blake3_hash_many(parents_array, parents_array_len, 1, key,
0, // Parents always use counter 0.
false, flags | PARENT,
0, // Parents have no start flags.
0, // Parents have no end flags.
out);
// If there's an odd child left over, it becomes an output.
if (num_chaining_values > 2 * parents_array_len) {
memcpy(&out[parents_array_len * BLAKE3_OUT_LEN],
&child_chaining_values[2 * parents_array_len * BLAKE3_OUT_LEN],
BLAKE3_OUT_LEN);
return parents_array_len + 1;
} else {
return parents_array_len;
}
}
// The wide helper function returns (writes out) an array of chaining values
// and returns the length of that array. The number of chaining values returned
// is the dynamically detected SIMD degree, at most MAX_SIMD_DEGREE. Or fewer,
// if the input is shorter than that many chunks. The reason for maintaining a
// wide array of chaining values going back up the tree, is to allow the
// implementation to hash as many parents in parallel as possible.
//
// As a special case when the SIMD degree is 1, this function will still return
// at least 2 outputs. This guarantees that this function doesn't perform the
// root compression. (If it did, it would use the wrong flags, and also we
// wouldn't be able to implement extendable output.) Note that this function is
// not used when the whole input is only 1 chunk long; that's a different
// codepath.
//
// Why not just have the caller split the input on the first update(), instead
// of implementing this special rule? Because we don't want to limit SIMD or
// multi-threading parallelism for that update().
static size_t blake3_compress_subtree_wide(const uint8_t *input,
size_t input_len,
const uint32_t key[8],
uint64_t chunk_counter,
uint8_t flags, uint8_t *out) {
// Note that the single chunk case does *not* bump the SIMD degree up to 2
// when it is 1. If this implementation adds multi-threading in the future,
// this gives us the option of multi-threading even the 2-chunk case, which
// can help performance on smaller platforms.
if (input_len <= blake3_simd_degree() * BLAKE3_CHUNK_LEN) {
return compress_chunks_parallel(input, input_len, key, chunk_counter, flags,
out);
}
// With more than simd_degree chunks, we need to recurse. Start by dividing
// the input into left and right subtrees. (Note that this is only optimal
// as long as the SIMD degree is a power of 2. If we ever get a SIMD degree
// of 3 or something, we'll need a more complicated strategy.)
size_t left_input_len = left_len(input_len);
size_t right_input_len = input_len - left_input_len;
const uint8_t *right_input = &input[left_input_len];
uint64_t right_chunk_counter =
chunk_counter + (uint64_t)(left_input_len / BLAKE3_CHUNK_LEN);
// Make space for the child outputs. Here we use MAX_SIMD_DEGREE_OR_2 to
// account for the special case of returning 2 outputs when the SIMD degree
// is 1.
uint8_t cv_array[2 * MAX_SIMD_DEGREE_OR_2 * BLAKE3_OUT_LEN];
size_t degree = blake3_simd_degree();
if (left_input_len > BLAKE3_CHUNK_LEN && degree == 1) {
// The special case: We always use a degree of at least two, to make
// sure there are two outputs. Except, as noted above, at the chunk
// level, where we allow degree=1. (Note that the 1-chunk-input case is
// a different codepath.)
degree = 2;
}
uint8_t *right_cvs = &cv_array[degree * BLAKE3_OUT_LEN];
// Recurse! If this implementation adds multi-threading support in the
// future, this is where it will go.
size_t left_n = blake3_compress_subtree_wide(input, left_input_len, key,
chunk_counter, flags, cv_array);
size_t right_n = blake3_compress_subtree_wide(
right_input, right_input_len, key, right_chunk_counter, flags, right_cvs);
// The special case again. If simd_degree=1, then we'll have left_n=1 and
// right_n=1. Rather than compressing them into a single output, return
// them directly, to make sure we always have at least two outputs.
if (left_n == 1) {
memcpy(out, cv_array, 2 * BLAKE3_OUT_LEN);
return 2;
}
// Otherwise, do one layer of parent node compression.
size_t num_chaining_values = left_n + right_n;
return compress_parents_parallel(cv_array, num_chaining_values, key, flags,
out);
}
// Hash a subtree with compress_subtree_wide(), and then condense the resulting
// list of chaining values down to a single parent node. Don't compress that
// last parent node, however. Instead, return its message bytes (the
// concatenated chaining values of its children). This is necessary when the
// first call to update() supplies a complete subtree, because the topmost
// parent node of that subtree could end up being the root. It's also necessary
// for extended output in the general case.
//
// As with compress_subtree_wide(), this function is not used on inputs of 1
// chunk or less. That's a different codepath.
INLINE void compress_subtree_to_parent_node(
const uint8_t *input, size_t input_len, const uint32_t key[8],
uint64_t chunk_counter, uint8_t flags, uint8_t out[2 * BLAKE3_OUT_LEN]) {
#if defined(BLAKE3_TESTING)
assert(input_len > BLAKE3_CHUNK_LEN);
#endif
uint8_t cv_array[MAX_SIMD_DEGREE_OR_2 * BLAKE3_OUT_LEN];
size_t num_cvs = blake3_compress_subtree_wide(input, input_len, key,
chunk_counter, flags, cv_array);
assert(num_cvs <= MAX_SIMD_DEGREE_OR_2);
// The following loop never executes when MAX_SIMD_DEGREE_OR_2 is 2, because
// as we just asserted, num_cvs will always be <=2 in that case. But GCC
// (particularly GCC 8.5) can't tell that it never executes, and if NDEBUG is
// set then it emits incorrect warnings here. We tried a few different
// hacks to silence these, but in the end our hacks just produced different
// warnings (see https://github.com/BLAKE3-team/BLAKE3/pull/380). Out of
// desperation, we ifdef out this entire loop when we know it's not needed.
#if MAX_SIMD_DEGREE_OR_2 > 2
// If MAX_SIMD_DEGREE_OR_2 is greater than 2 and there's enough input,
// compress_subtree_wide() returns more than 2 chaining values. Condense
// them into 2 by forming parent nodes repeatedly.
uint8_t out_array[MAX_SIMD_DEGREE_OR_2 * BLAKE3_OUT_LEN / 2];
while (num_cvs > 2) {
num_cvs =
compress_parents_parallel(cv_array, num_cvs, key, flags, out_array);
memcpy(cv_array, out_array, num_cvs * BLAKE3_OUT_LEN);
}
#endif
memcpy(out, cv_array, 2 * BLAKE3_OUT_LEN);
}
INLINE void hasher_init_base(blake3_hasher *self, const uint32_t key[8],
uint8_t flags) {
memcpy(self->key, key, BLAKE3_KEY_LEN);
chunk_state_init(&self->chunk, key, flags);
self->cv_stack_len = 0;
}
void blake3_hasher_init(blake3_hasher *self) { hasher_init_base(self, IV, 0); }
void blake3_hasher_init_keyed(blake3_hasher *self,
const uint8_t key[BLAKE3_KEY_LEN]) {
uint32_t key_words[8];
load_key_words(key, key_words);
hasher_init_base(self, key_words, KEYED_HASH);
}
void blake3_hasher_init_derive_key_raw(blake3_hasher *self, const void *context,
size_t context_len) {
blake3_hasher context_hasher;
hasher_init_base(&context_hasher, IV, DERIVE_KEY_CONTEXT);
blake3_hasher_update(&context_hasher, context, context_len);
uint8_t context_key[BLAKE3_KEY_LEN];
blake3_hasher_finalize(&context_hasher, context_key, BLAKE3_KEY_LEN);
uint32_t context_key_words[8];
load_key_words(context_key, context_key_words);
hasher_init_base(self, context_key_words, DERIVE_KEY_MATERIAL);
}
void blake3_hasher_init_derive_key(blake3_hasher *self, const char *context) {
blake3_hasher_init_derive_key_raw(self, context, strlen(context));
}
// As described in hasher_push_cv() below, we do "lazy merging", delaying
// merges until right before the next CV is about to be added. This is
// different from the reference implementation. Another difference is that we
// aren't always merging 1 chunk at a time. Instead, each CV might represent
// any power-of-two number of chunks, as long as the smaller-above-larger stack
// order is maintained. Instead of the "count the trailing 0-bits" algorithm
// described in the spec, we use a "count the total number of 1-bits" variant
// that doesn't require us to retain the subtree size of the CV on top of the
// stack. The principle is the same: each CV that should remain in the stack is
// represented by a 1-bit in the total number of chunks (or bytes) so far.
INLINE void hasher_merge_cv_stack(blake3_hasher *self, uint64_t total_len) {
size_t post_merge_stack_len = (size_t)popcnt(total_len);
while (self->cv_stack_len > post_merge_stack_len) {
uint8_t *parent_node =
&self->cv_stack[(self->cv_stack_len - 2) * BLAKE3_OUT_LEN];
output_t output = parent_output(parent_node, self->key, self->chunk.flags);
output_chaining_value(&output, parent_node);
self->cv_stack_len -= 1;
}
}
// In reference_impl.rs, we merge the new CV with existing CVs from the stack
// before pushing it. We can do that because we know more input is coming, so
// we know none of the merges are root.
//
// This setting is different. We want to feed as much input as possible to
// compress_subtree_wide(), without setting aside anything for the chunk_state.
// If the user gives us 64 KiB, we want to parallelize over all 64 KiB at once
// as a single subtree, if at all possible.
//
// This leads to two problems:
// 1) This 64 KiB input might be the only call that ever gets made to update.
// In this case, the root node of the 64 KiB subtree would be the root node
// of the whole tree, and it would need to be ROOT finalized. We can't
// compress it until we know.
// 2) This 64 KiB input might complete a larger tree, whose root node is
// similarly going to be the root of the whole tree. For example, maybe
// we have 196 KiB (that is, 128 + 64) hashed so far. We can't compress the
// node at the root of the 256 KiB subtree until we know how to finalize it.
//
// The second problem is solved with "lazy merging". That is, when we're about
// to add a CV to the stack, we don't merge it with anything first, as the
// reference impl does. Instead we do merges using the *previous* CV that was
// added, which is sitting on top of the stack, and we put the new CV
// (unmerged) on top of the stack afterwards. This guarantees that we never
// merge the root node until finalize().
//
// Solving the first problem requires an additional tool,
// compress_subtree_to_parent_node(). That function always returns the top
// *two* chaining values of the subtree it's compressing. We then do lazy
// merging with each of them separately, so that the second CV will always
// remain unmerged. (That also helps us support extendable output when we're
// hashing an input all-at-once.)
INLINE void hasher_push_cv(blake3_hasher *self, uint8_t new_cv[BLAKE3_OUT_LEN],
uint64_t chunk_counter) {
hasher_merge_cv_stack(self, chunk_counter);
memcpy(&self->cv_stack[self->cv_stack_len * BLAKE3_OUT_LEN], new_cv,
BLAKE3_OUT_LEN);
self->cv_stack_len += 1;
}
void blake3_hasher_update(blake3_hasher *self, const void *input,
size_t input_len) {
// Explicitly checking for zero avoids causing UB by passing a null pointer
// to memcpy. This comes up in practice with things like:
// std::vector<uint8_t> v;
// blake3_hasher_update(&hasher, v.data(), v.size());
if (input_len == 0) {
return;
}
const uint8_t *input_bytes = (const uint8_t *)input;
// If we have some partial chunk bytes in the internal chunk_state, we need
// to finish that chunk first.
if (chunk_state_len(&self->chunk) > 0) {
size_t take = BLAKE3_CHUNK_LEN - chunk_state_len(&self->chunk);
if (take > input_len) {
take = input_len;
}
chunk_state_update(&self->chunk, input_bytes, take);
input_bytes += take;
input_len -= take;
// If we've filled the current chunk and there's more coming, finalize this
// chunk and proceed. In this case we know it's not the root.
if (input_len > 0) {
output_t output = chunk_state_output(&self->chunk);
uint8_t chunk_cv[32];
output_chaining_value(&output, chunk_cv);
hasher_push_cv(self, chunk_cv, self->chunk.chunk_counter);
chunk_state_reset(&self->chunk, self->key, self->chunk.chunk_counter + 1);
} else {
return;
}
}
// Now the chunk_state is clear, and we have more input. If there's more than
// a single chunk (so, definitely not the root chunk), hash the largest whole
// subtree we can, with the full benefits of SIMD (and maybe in the future,
// multi-threading) parallelism. Two restrictions:
// - The subtree has to be a power-of-2 number of chunks. Only subtrees along
// the right edge can be incomplete, and we don't know where the right edge
// is going to be until we get to finalize().
// - The subtree must evenly divide the total number of chunks up until this
// point (if total is not 0). If the current incomplete subtree is only
// waiting for 1 more chunk, we can't hash a subtree of 4 chunks. We have
// to complete the current subtree first.
// Because we might need to break up the input to form powers of 2, or to
// evenly divide what we already have, this part runs in a loop.
while (input_len > BLAKE3_CHUNK_LEN) {
size_t subtree_len = round_down_to_power_of_2(input_len);
uint64_t count_so_far = self->chunk.chunk_counter * BLAKE3_CHUNK_LEN;
// Shrink the subtree_len until it evenly divides the count so far. We know
// that subtree_len itself is a power of 2, so we can use a bitmasking
// trick instead of an actual remainder operation. (Note that if the caller
// consistently passes power-of-2 inputs of the same size, as is hopefully
// typical, this loop condition will always fail, and subtree_len will
// always be the full length of the input.)
//
// An aside: We don't have to shrink subtree_len quite this much. For
// example, if count_so_far is 1, we could pass 2 chunks to
// compress_subtree_to_parent_node. Since we'll get 2 CVs back, we'll still
// get the right answer in the end, and we might get to use 2-way SIMD
// parallelism. The problem with this optimization, is that it gets us
// stuck always hashing 2 chunks. The total number of chunks will remain
// odd, and we'll never graduate to higher degrees of parallelism. See
// https://github.com/BLAKE3-team/BLAKE3/issues/69.
while ((((uint64_t)(subtree_len - 1)) & count_so_far) != 0) {
subtree_len /= 2;
}
// The shrunken subtree_len might now be 1 chunk long. If so, hash that one
// chunk by itself. Otherwise, compress the subtree into a pair of CVs.
uint64_t subtree_chunks = subtree_len / BLAKE3_CHUNK_LEN;
if (subtree_len <= BLAKE3_CHUNK_LEN) {
blake3_chunk_state chunk_state;
chunk_state_init(&chunk_state, self->key, self->chunk.flags);
chunk_state.chunk_counter = self->chunk.chunk_counter;
chunk_state_update(&chunk_state, input_bytes, subtree_len);
output_t output = chunk_state_output(&chunk_state);
uint8_t cv[BLAKE3_OUT_LEN];
output_chaining_value(&output, cv);
hasher_push_cv(self, cv, chunk_state.chunk_counter);
} else {
// This is the high-performance happy path, though getting here depends
// on the caller giving us a long enough input.
uint8_t cv_pair[2 * BLAKE3_OUT_LEN];
compress_subtree_to_parent_node(input_bytes, subtree_len, self->key,
self->chunk.chunk_counter,
self->chunk.flags, cv_pair);
hasher_push_cv(self, cv_pair, self->chunk.chunk_counter);
hasher_push_cv(self, &cv_pair[BLAKE3_OUT_LEN],
self->chunk.chunk_counter + (subtree_chunks / 2));
}
self->chunk.chunk_counter += subtree_chunks;
input_bytes += subtree_len;
input_len -= subtree_len;
}
// If there's any remaining input less than a full chunk, add it to the chunk
// state. In that case, also do a final merge loop to make sure the subtree
// stack doesn't contain any unmerged pairs. The remaining input means we
// know these merges are non-root. This merge loop isn't strictly necessary
// here, because hasher_push_chunk_cv already does its own merge loop, but it
// simplifies blake3_hasher_finalize below.
if (input_len > 0) {
chunk_state_update(&self->chunk, input_bytes, input_len);
hasher_merge_cv_stack(self, self->chunk.chunk_counter);
}
}
void blake3_hasher_finalize(const blake3_hasher *self, uint8_t *out,
size_t out_len) {
blake3_hasher_finalize_seek(self, 0, out, out_len);
}
void blake3_hasher_finalize_seek(const blake3_hasher *self, uint64_t seek,
uint8_t *out, size_t out_len) {
// Explicitly checking for zero avoids causing UB by passing a null pointer
// to memcpy. This comes up in practice with things like:
// std::vector<uint8_t> v;
// blake3_hasher_finalize(&hasher, v.data(), v.size());
if (out_len == 0) {
return;
}
// If the subtree stack is empty, then the current chunk is the root.
if (self->cv_stack_len == 0) {
output_t output = chunk_state_output(&self->chunk);
output_root_bytes(&output, seek, out, out_len);
return;
}
// If there are any bytes in the chunk state, finalize that chunk and do a
// roll-up merge between that chunk hash and every subtree in the stack. In
// this case, the extra merge loop at the end of blake3_hasher_update
// guarantees that none of the subtrees in the stack need to be merged with
// each other first. Otherwise, if there are no bytes in the chunk state,
// then the top of the stack is a chunk hash, and we start the merge from
// that.
output_t output;
size_t cvs_remaining;
if (chunk_state_len(&self->chunk) > 0) {
cvs_remaining = self->cv_stack_len;
output = chunk_state_output(&self->chunk);
} else {
// There are always at least 2 CVs in the stack in this case.
cvs_remaining = self->cv_stack_len - 2;
output = parent_output(&self->cv_stack[cvs_remaining * 32], self->key,
self->chunk.flags);
}
while (cvs_remaining > 0) {
cvs_remaining -= 1;
uint8_t parent_block[BLAKE3_BLOCK_LEN];
memcpy(parent_block, &self->cv_stack[cvs_remaining * 32], 32);
output_chaining_value(&output, &parent_block[32]);
output = parent_output(parent_block, self->key, self->chunk.flags);
}
output_root_bytes(&output, seek, out, out_len);
}
void blake3_hasher_reset(blake3_hasher *self) {
chunk_state_reset(&self->chunk, self->key, 0);
self->cv_stack_len = 0;
}
+82
View File
@@ -0,0 +1,82 @@
#ifndef BLAKE3_H
#define BLAKE3_H
#include <stddef.h>
#include <stdint.h>
#if !defined(BLAKE3_API)
# if defined(_WIN32) || defined(__CYGWIN__)
# if defined(BLAKE3_DLL)
# if defined(BLAKE3_DLL_EXPORTS)
# define BLAKE3_API __declspec(dllexport)
# else
# define BLAKE3_API __declspec(dllimport)
# endif
# define BLAKE3_PRIVATE
# else
# define BLAKE3_API
# define BLAKE3_PRIVATE
# endif
# elif __GNUC__ >= 4
# define BLAKE3_API __attribute__((visibility("default")))
# define BLAKE3_PRIVATE __attribute__((visibility("hidden")))
# else
# define BLAKE3_API
# define BLAKE3_PRIVATE
# endif
#endif
#ifdef __cplusplus
extern "C" {
#endif
#define BLAKE3_VERSION_STRING "1.5.5"
#define BLAKE3_KEY_LEN 32
#define BLAKE3_OUT_LEN 32
#define BLAKE3_BLOCK_LEN 64
#define BLAKE3_CHUNK_LEN 1024
#define BLAKE3_MAX_DEPTH 54
// This struct is a private implementation detail. It has to be here because
// it's part of blake3_hasher below.
typedef struct {
uint32_t cv[8];
uint64_t chunk_counter;
uint8_t buf[BLAKE3_BLOCK_LEN];
uint8_t buf_len;
uint8_t blocks_compressed;
uint8_t flags;
} blake3_chunk_state;
typedef struct {
uint32_t key[8];
blake3_chunk_state chunk;
uint8_t cv_stack_len;
// The stack size is MAX_DEPTH + 1 because we do lazy merging. For example,
// with 7 chunks, we have 3 entries in the stack. Adding an 8th chunk
// requires a 4th entry, rather than merging everything down to 1, because we
// don't know whether more input is coming. This is different from how the
// reference implementation does things.
uint8_t cv_stack[(BLAKE3_MAX_DEPTH + 1) * BLAKE3_OUT_LEN];
} blake3_hasher;
BLAKE3_API const char *blake3_version(void);
BLAKE3_API void blake3_hasher_init(blake3_hasher *self);
BLAKE3_API void blake3_hasher_init_keyed(blake3_hasher *self,
const uint8_t key[BLAKE3_KEY_LEN]);
BLAKE3_API void blake3_hasher_init_derive_key(blake3_hasher *self, const char *context);
BLAKE3_API void blake3_hasher_init_derive_key_raw(blake3_hasher *self, const void *context,
size_t context_len);
BLAKE3_API void blake3_hasher_update(blake3_hasher *self, const void *input,
size_t input_len);
BLAKE3_API void blake3_hasher_finalize(const blake3_hasher *self, uint8_t *out,
size_t out_len);
BLAKE3_API void blake3_hasher_finalize_seek(const blake3_hasher *self, uint64_t seek,
uint8_t *out, size_t out_len);
BLAKE3_API void blake3_hasher_reset(blake3_hasher *self);
#ifdef __cplusplus
}
#endif
#endif /* BLAKE3_H */
+331
View File
@@ -0,0 +1,331 @@
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include "blake3_impl.h"
#if defined(_MSC_VER)
#include <Windows.h>
#endif
#if defined(IS_X86)
#if defined(_MSC_VER)
#include <intrin.h>
#elif defined(__GNUC__)
#include <immintrin.h>
#else
#undef IS_X86 /* Unimplemented! */
#endif
#endif
#if !defined(BLAKE3_ATOMICS)
#if defined(__has_include)
#if __has_include(<stdatomic.h>) && !defined(_MSC_VER)
#define BLAKE3_ATOMICS 1
#else
#define BLAKE3_ATOMICS 0
#endif /* __has_include(<stdatomic.h>) && !defined(_MSC_VER) */
#else
#define BLAKE3_ATOMICS 0
#endif /* defined(__has_include) */
#endif /* BLAKE3_ATOMICS */
#if BLAKE3_ATOMICS
#define ATOMIC_INT _Atomic int
#define ATOMIC_LOAD(x) x
#define ATOMIC_STORE(x, y) x = y
#elif defined(_MSC_VER)
#define ATOMIC_INT LONG
#define ATOMIC_LOAD(x) InterlockedOr(&x, 0)
#define ATOMIC_STORE(x, y) InterlockedExchange(&x, y)
#else
#define ATOMIC_INT int
#define ATOMIC_LOAD(x) x
#define ATOMIC_STORE(x, y) x = y
#endif
#define MAYBE_UNUSED(x) (void)((x))
#if defined(IS_X86)
static uint64_t xgetbv(void) {
#if defined(_MSC_VER)
return _xgetbv(0);
#else
uint32_t eax = 0, edx = 0;
__asm__ __volatile__("xgetbv\n" : "=a"(eax), "=d"(edx) : "c"(0));
return ((uint64_t)edx << 32) | eax;
#endif
}
static void cpuid(uint32_t out[4], uint32_t id) {
#if defined(_MSC_VER)
__cpuid((int *)out, id);
#elif defined(__i386__) || defined(_M_IX86)
__asm__ __volatile__("movl %%ebx, %1\n"
"cpuid\n"
"xchgl %1, %%ebx\n"
: "=a"(out[0]), "=r"(out[1]), "=c"(out[2]), "=d"(out[3])
: "a"(id));
#else
__asm__ __volatile__("cpuid\n"
: "=a"(out[0]), "=b"(out[1]), "=c"(out[2]), "=d"(out[3])
: "a"(id));
#endif
}
static void cpuidex(uint32_t out[4], uint32_t id, uint32_t sid) {
#if defined(_MSC_VER)
__cpuidex((int *)out, id, sid);
#elif defined(__i386__) || defined(_M_IX86)
__asm__ __volatile__("movl %%ebx, %1\n"
"cpuid\n"
"xchgl %1, %%ebx\n"
: "=a"(out[0]), "=r"(out[1]), "=c"(out[2]), "=d"(out[3])
: "a"(id), "c"(sid));
#else
__asm__ __volatile__("cpuid\n"
: "=a"(out[0]), "=b"(out[1]), "=c"(out[2]), "=d"(out[3])
: "a"(id), "c"(sid));
#endif
}
#endif
enum cpu_feature {
SSE2 = 1 << 0,
SSSE3 = 1 << 1,
SSE41 = 1 << 2,
AVX = 1 << 3,
AVX2 = 1 << 4,
AVX512F = 1 << 5,
AVX512VL = 1 << 6,
/* ... */
UNDEFINED = 1 << 30
};
#if !defined(BLAKE3_TESTING)
static /* Allow the variable to be controlled manually for testing */
#endif
ATOMIC_INT g_cpu_features = UNDEFINED;
#if !defined(BLAKE3_TESTING)
static
#endif
enum cpu_feature
get_cpu_features(void) {
/* If TSAN detects a data race here, try compiling with -DBLAKE3_ATOMICS=1 */
enum cpu_feature features = ATOMIC_LOAD(g_cpu_features);
if (features != UNDEFINED) {
return features;
} else {
#if defined(IS_X86)
uint32_t regs[4] = {0};
uint32_t *eax = &regs[0], *ebx = &regs[1], *ecx = &regs[2], *edx = &regs[3];
(void)edx;
features = 0;
cpuid(regs, 0);
const int max_id = *eax;
cpuid(regs, 1);
#if defined(__amd64__) || defined(_M_X64)
features |= SSE2;
#else
if (*edx & (1UL << 26))
features |= SSE2;
#endif
if (*ecx & (1UL << 9))
features |= SSSE3;
if (*ecx & (1UL << 19))
features |= SSE41;
if (*ecx & (1UL << 27)) { // OSXSAVE
const uint64_t mask = xgetbv();
if ((mask & 6) == 6) { // SSE and AVX states
if (*ecx & (1UL << 28))
features |= AVX;
if (max_id >= 7) {
cpuidex(regs, 7, 0);
if (*ebx & (1UL << 5))
features |= AVX2;
if ((mask & 224) == 224) { // Opmask, ZMM_Hi256, Hi16_Zmm
if (*ebx & (1UL << 31))
features |= AVX512VL;
if (*ebx & (1UL << 16))
features |= AVX512F;
}
}
}
}
ATOMIC_STORE(g_cpu_features, features);
return features;
#else
/* How to detect NEON? */
return 0;
#endif
}
}
void blake3_compress_in_place(uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter,
uint8_t flags) {
#if defined(IS_X86)
const enum cpu_feature features = get_cpu_features();
MAYBE_UNUSED(features);
#if !defined(BLAKE3_NO_AVX512)
if (features & AVX512VL) {
blake3_compress_in_place_avx512(cv, block, block_len, counter, flags);
return;
}
#endif
#if !defined(BLAKE3_NO_SSE41)
if (features & SSE41) {
blake3_compress_in_place_sse41(cv, block, block_len, counter, flags);
return;
}
#endif
#if !defined(BLAKE3_NO_SSE2)
if (features & SSE2) {
blake3_compress_in_place_sse2(cv, block, block_len, counter, flags);
return;
}
#endif
#endif
blake3_compress_in_place_portable(cv, block, block_len, counter, flags);
}
void blake3_compress_xof(const uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter, uint8_t flags,
uint8_t out[64]) {
#if defined(IS_X86)
const enum cpu_feature features = get_cpu_features();
MAYBE_UNUSED(features);
#if !defined(BLAKE3_NO_AVX512)
if (features & AVX512VL) {
blake3_compress_xof_avx512(cv, block, block_len, counter, flags, out);
return;
}
#endif
#if !defined(BLAKE3_NO_SSE41)
if (features & SSE41) {
blake3_compress_xof_sse41(cv, block, block_len, counter, flags, out);
return;
}
#endif
#if !defined(BLAKE3_NO_SSE2)
if (features & SSE2) {
blake3_compress_xof_sse2(cv, block, block_len, counter, flags, out);
return;
}
#endif
#endif
blake3_compress_xof_portable(cv, block, block_len, counter, flags, out);
}
void blake3_xof_many(const uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter, uint8_t flags,
uint8_t out[64], size_t outblocks) {
if (outblocks == 0) {
// The current assembly implementation always outputs at least 1 block.
return;
}
#if defined(IS_X86)
const enum cpu_feature features = get_cpu_features();
#if !defined(_WIN32) && !defined(BLAKE3_NO_AVX512)
if (features & AVX512VL) {
blake3_xof_many_avx512(cv, block, block_len, counter, flags, out, outblocks);
return;
}
#endif
#endif
for(size_t i = 0; i < outblocks; ++i) {
blake3_compress_xof(cv, block, block_len, counter + i, flags, out + 64*i);
}
}
void blake3_hash_many(const uint8_t *const *inputs, size_t num_inputs,
size_t blocks, const uint32_t key[8], uint64_t counter,
bool increment_counter, uint8_t flags,
uint8_t flags_start, uint8_t flags_end, uint8_t *out) {
#if defined(IS_X86)
const enum cpu_feature features = get_cpu_features();
MAYBE_UNUSED(features);
#if !defined(BLAKE3_NO_AVX512)
if ((features & (AVX512F|AVX512VL)) == (AVX512F|AVX512VL)) {
blake3_hash_many_avx512(inputs, num_inputs, blocks, key, counter,
increment_counter, flags, flags_start, flags_end,
out);
return;
}
#endif
#if !defined(BLAKE3_NO_AVX2)
if (features & AVX2) {
blake3_hash_many_avx2(inputs, num_inputs, blocks, key, counter,
increment_counter, flags, flags_start, flags_end,
out);
return;
}
#endif
#if !defined(BLAKE3_NO_SSE41)
if (features & SSE41) {
blake3_hash_many_sse41(inputs, num_inputs, blocks, key, counter,
increment_counter, flags, flags_start, flags_end,
out);
return;
}
#endif
#if !defined(BLAKE3_NO_SSE2)
if (features & SSE2) {
blake3_hash_many_sse2(inputs, num_inputs, blocks, key, counter,
increment_counter, flags, flags_start, flags_end,
out);
return;
}
#endif
#endif
#if BLAKE3_USE_NEON == 1
blake3_hash_many_neon(inputs, num_inputs, blocks, key, counter,
increment_counter, flags, flags_start, flags_end, out);
return;
#endif
blake3_hash_many_portable(inputs, num_inputs, blocks, key, counter,
increment_counter, flags, flags_start, flags_end,
out);
}
// The dynamically detected SIMD degree of the current platform.
size_t blake3_simd_degree(void) {
#if defined(IS_X86)
const enum cpu_feature features = get_cpu_features();
MAYBE_UNUSED(features);
#if !defined(BLAKE3_NO_AVX512)
if ((features & (AVX512F|AVX512VL)) == (AVX512F|AVX512VL)) {
return 16;
}
#endif
#if !defined(BLAKE3_NO_AVX2)
if (features & AVX2) {
return 8;
}
#endif
#if !defined(BLAKE3_NO_SSE41)
if (features & SSE41) {
return 4;
}
#endif
#if !defined(BLAKE3_NO_SSE2)
if (features & SSE2) {
return 4;
}
#endif
#endif
#if BLAKE3_USE_NEON == 1
return 4;
#endif
return 1;
}
+304
View File
@@ -0,0 +1,304 @@
#ifndef BLAKE3_IMPL_H
#define BLAKE3_IMPL_H
#include <assert.h>
#include <stdbool.h>
#include <stddef.h>
#include <stdint.h>
#include <string.h>
#include "blake3.h"
// internal flags
enum blake3_flags {
CHUNK_START = 1 << 0,
CHUNK_END = 1 << 1,
PARENT = 1 << 2,
ROOT = 1 << 3,
KEYED_HASH = 1 << 4,
DERIVE_KEY_CONTEXT = 1 << 5,
DERIVE_KEY_MATERIAL = 1 << 6,
};
// This C implementation tries to support recent versions of GCC, Clang, and
// MSVC.
#if defined(_MSC_VER)
#define INLINE static __forceinline
#else
#define INLINE static inline __attribute__((always_inline))
#endif
#if (defined(__x86_64__) || defined(_M_X64)) && !defined(_M_ARM64EC)
#define IS_X86
#define IS_X86_64
#endif
#if defined(__i386__) || defined(_M_IX86)
#define IS_X86
#define IS_X86_32
#endif
#if defined(__aarch64__) || defined(_M_ARM64) || defined(_M_ARM64EC)
#define IS_AARCH64
#endif
#if defined(IS_X86)
#if defined(_MSC_VER)
#include <intrin.h>
#endif
#endif
#if !defined(BLAKE3_USE_NEON)
// If BLAKE3_USE_NEON not manually set, autodetect based on AArch64ness
#if defined(IS_AARCH64)
#if defined(__ARM_BIG_ENDIAN)
#define BLAKE3_USE_NEON 0
#else
#define BLAKE3_USE_NEON 1
#endif
#else
#define BLAKE3_USE_NEON 0
#endif
#endif
#if defined(IS_X86)
#define MAX_SIMD_DEGREE 16
#elif BLAKE3_USE_NEON == 1
#define MAX_SIMD_DEGREE 4
#else
#define MAX_SIMD_DEGREE 1
#endif
// There are some places where we want a static size that's equal to the
// MAX_SIMD_DEGREE, but also at least 2.
#define MAX_SIMD_DEGREE_OR_2 (MAX_SIMD_DEGREE > 2 ? MAX_SIMD_DEGREE : 2)
static const uint32_t IV[8] = {0x6A09E667UL, 0xBB67AE85UL, 0x3C6EF372UL,
0xA54FF53AUL, 0x510E527FUL, 0x9B05688CUL,
0x1F83D9ABUL, 0x5BE0CD19UL};
static const uint8_t MSG_SCHEDULE[7][16] = {
{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15},
{2, 6, 3, 10, 7, 0, 4, 13, 1, 11, 12, 5, 9, 14, 15, 8},
{3, 4, 10, 12, 13, 2, 7, 14, 6, 5, 9, 0, 11, 15, 8, 1},
{10, 7, 12, 9, 14, 3, 13, 15, 4, 0, 11, 2, 5, 8, 1, 6},
{12, 13, 9, 11, 15, 10, 14, 8, 7, 2, 5, 3, 0, 1, 6, 4},
{9, 14, 11, 5, 8, 12, 15, 1, 13, 3, 0, 10, 2, 6, 4, 7},
{11, 15, 5, 0, 1, 9, 8, 6, 14, 10, 2, 12, 3, 4, 7, 13},
};
/* Find index of the highest set bit */
/* x is assumed to be nonzero. */
static unsigned int highest_one(uint64_t x) {
#if defined(__GNUC__) || defined(__clang__)
return 63 ^ (unsigned int)__builtin_clzll(x);
#elif defined(_MSC_VER) && defined(IS_X86_64)
unsigned long index;
_BitScanReverse64(&index, x);
return index;
#elif defined(_MSC_VER) && defined(IS_X86_32)
if(x >> 32) {
unsigned long index;
_BitScanReverse(&index, (unsigned long)(x >> 32));
return 32 + index;
} else {
unsigned long index;
_BitScanReverse(&index, (unsigned long)x);
return index;
}
#else
unsigned int c = 0;
if(x & 0xffffffff00000000ULL) { x >>= 32; c += 32; }
if(x & 0x00000000ffff0000ULL) { x >>= 16; c += 16; }
if(x & 0x000000000000ff00ULL) { x >>= 8; c += 8; }
if(x & 0x00000000000000f0ULL) { x >>= 4; c += 4; }
if(x & 0x000000000000000cULL) { x >>= 2; c += 2; }
if(x & 0x0000000000000002ULL) { c += 1; }
return c;
#endif
}
// Count the number of 1 bits.
INLINE unsigned int popcnt(uint64_t x) {
#if defined(__GNUC__) || defined(__clang__)
return (unsigned int)__builtin_popcountll(x);
#else
unsigned int count = 0;
while (x != 0) {
count += 1;
x &= x - 1;
}
return count;
#endif
}
// Largest power of two less than or equal to x. As a special case, returns 1
// when x is 0.
INLINE uint64_t round_down_to_power_of_2(uint64_t x) {
return 1ULL << highest_one(x | 1);
}
INLINE uint32_t counter_low(uint64_t counter) { return (uint32_t)counter; }
INLINE uint32_t counter_high(uint64_t counter) {
return (uint32_t)(counter >> 32);
}
INLINE uint32_t load32(const void *src) {
const uint8_t *p = (const uint8_t *)src;
return ((uint32_t)(p[0]) << 0) | ((uint32_t)(p[1]) << 8) |
((uint32_t)(p[2]) << 16) | ((uint32_t)(p[3]) << 24);
}
INLINE void load_key_words(const uint8_t key[BLAKE3_KEY_LEN],
uint32_t key_words[8]) {
key_words[0] = load32(&key[0 * 4]);
key_words[1] = load32(&key[1 * 4]);
key_words[2] = load32(&key[2 * 4]);
key_words[3] = load32(&key[3 * 4]);
key_words[4] = load32(&key[4 * 4]);
key_words[5] = load32(&key[5 * 4]);
key_words[6] = load32(&key[6 * 4]);
key_words[7] = load32(&key[7 * 4]);
}
INLINE void load_block_words(const uint8_t block[BLAKE3_BLOCK_LEN],
uint32_t block_words[16]) {
for (size_t i = 0; i < 16; i++) {
block_words[i] = load32(&block[i * 4]);
}
}
INLINE void store32(void *dst, uint32_t w) {
uint8_t *p = (uint8_t *)dst;
p[0] = (uint8_t)(w >> 0);
p[1] = (uint8_t)(w >> 8);
p[2] = (uint8_t)(w >> 16);
p[3] = (uint8_t)(w >> 24);
}
INLINE void store_cv_words(uint8_t bytes_out[32], uint32_t cv_words[8]) {
store32(&bytes_out[0 * 4], cv_words[0]);
store32(&bytes_out[1 * 4], cv_words[1]);
store32(&bytes_out[2 * 4], cv_words[2]);
store32(&bytes_out[3 * 4], cv_words[3]);
store32(&bytes_out[4 * 4], cv_words[4]);
store32(&bytes_out[5 * 4], cv_words[5]);
store32(&bytes_out[6 * 4], cv_words[6]);
store32(&bytes_out[7 * 4], cv_words[7]);
}
void blake3_compress_in_place(uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter,
uint8_t flags);
void blake3_compress_xof(const uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter, uint8_t flags,
uint8_t out[64]);
void blake3_xof_many(const uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter, uint8_t flags,
uint8_t out[64], size_t outblocks);
void blake3_hash_many(const uint8_t *const *inputs, size_t num_inputs,
size_t blocks, const uint32_t key[8], uint64_t counter,
bool increment_counter, uint8_t flags,
uint8_t flags_start, uint8_t flags_end, uint8_t *out);
size_t blake3_simd_degree(void);
// Declarations for implementation-specific functions.
void blake3_compress_in_place_portable(uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter,
uint8_t flags);
void blake3_compress_xof_portable(const uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter,
uint8_t flags, uint8_t out[64]);
void blake3_hash_many_portable(const uint8_t *const *inputs, size_t num_inputs,
size_t blocks, const uint32_t key[8],
uint64_t counter, bool increment_counter,
uint8_t flags, uint8_t flags_start,
uint8_t flags_end, uint8_t *out);
#if defined(IS_X86)
#if !defined(BLAKE3_NO_SSE2)
void blake3_compress_in_place_sse2(uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter,
uint8_t flags);
void blake3_compress_xof_sse2(const uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter,
uint8_t flags, uint8_t out[64]);
void blake3_hash_many_sse2(const uint8_t *const *inputs, size_t num_inputs,
size_t blocks, const uint32_t key[8],
uint64_t counter, bool increment_counter,
uint8_t flags, uint8_t flags_start,
uint8_t flags_end, uint8_t *out);
#endif
#if !defined(BLAKE3_NO_SSE41)
void blake3_compress_in_place_sse41(uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter,
uint8_t flags);
void blake3_compress_xof_sse41(const uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter,
uint8_t flags, uint8_t out[64]);
void blake3_hash_many_sse41(const uint8_t *const *inputs, size_t num_inputs,
size_t blocks, const uint32_t key[8],
uint64_t counter, bool increment_counter,
uint8_t flags, uint8_t flags_start,
uint8_t flags_end, uint8_t *out);
#endif
#if !defined(BLAKE3_NO_AVX2)
void blake3_hash_many_avx2(const uint8_t *const *inputs, size_t num_inputs,
size_t blocks, const uint32_t key[8],
uint64_t counter, bool increment_counter,
uint8_t flags, uint8_t flags_start,
uint8_t flags_end, uint8_t *out);
#endif
#if !defined(BLAKE3_NO_AVX512)
void blake3_compress_in_place_avx512(uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter,
uint8_t flags);
void blake3_compress_xof_avx512(const uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter,
uint8_t flags, uint8_t out[64]);
void blake3_hash_many_avx512(const uint8_t *const *inputs, size_t num_inputs,
size_t blocks, const uint32_t key[8],
uint64_t counter, bool increment_counter,
uint8_t flags, uint8_t flags_start,
uint8_t flags_end, uint8_t *out);
#if !defined(_WIN32)
void blake3_xof_many_avx512(const uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter, uint8_t flags,
uint8_t* out, size_t outblocks);
#endif
#endif
#endif
#if BLAKE3_USE_NEON == 1
void blake3_hash_many_neon(const uint8_t *const *inputs, size_t num_inputs,
size_t blocks, const uint32_t key[8],
uint64_t counter, bool increment_counter,
uint8_t flags, uint8_t flags_start,
uint8_t flags_end, uint8_t *out);
#endif
#endif /* BLAKE3_IMPL_H */
+160
View File
@@ -0,0 +1,160 @@
#include "blake3_impl.h"
#include <string.h>
INLINE uint32_t rotr32(uint32_t w, uint32_t c) {
return (w >> c) | (w << (32 - c));
}
INLINE void g(uint32_t *state, size_t a, size_t b, size_t c, size_t d,
uint32_t x, uint32_t y) {
state[a] = state[a] + state[b] + x;
state[d] = rotr32(state[d] ^ state[a], 16);
state[c] = state[c] + state[d];
state[b] = rotr32(state[b] ^ state[c], 12);
state[a] = state[a] + state[b] + y;
state[d] = rotr32(state[d] ^ state[a], 8);
state[c] = state[c] + state[d];
state[b] = rotr32(state[b] ^ state[c], 7);
}
INLINE void round_fn(uint32_t state[16], const uint32_t *msg, size_t round) {
// Select the message schedule based on the round.
const uint8_t *schedule = MSG_SCHEDULE[round];
// Mix the columns.
g(state, 0, 4, 8, 12, msg[schedule[0]], msg[schedule[1]]);
g(state, 1, 5, 9, 13, msg[schedule[2]], msg[schedule[3]]);
g(state, 2, 6, 10, 14, msg[schedule[4]], msg[schedule[5]]);
g(state, 3, 7, 11, 15, msg[schedule[6]], msg[schedule[7]]);
// Mix the rows.
g(state, 0, 5, 10, 15, msg[schedule[8]], msg[schedule[9]]);
g(state, 1, 6, 11, 12, msg[schedule[10]], msg[schedule[11]]);
g(state, 2, 7, 8, 13, msg[schedule[12]], msg[schedule[13]]);
g(state, 3, 4, 9, 14, msg[schedule[14]], msg[schedule[15]]);
}
INLINE void compress_pre(uint32_t state[16], const uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter, uint8_t flags) {
uint32_t block_words[16];
block_words[0] = load32(block + 4 * 0);
block_words[1] = load32(block + 4 * 1);
block_words[2] = load32(block + 4 * 2);
block_words[3] = load32(block + 4 * 3);
block_words[4] = load32(block + 4 * 4);
block_words[5] = load32(block + 4 * 5);
block_words[6] = load32(block + 4 * 6);
block_words[7] = load32(block + 4 * 7);
block_words[8] = load32(block + 4 * 8);
block_words[9] = load32(block + 4 * 9);
block_words[10] = load32(block + 4 * 10);
block_words[11] = load32(block + 4 * 11);
block_words[12] = load32(block + 4 * 12);
block_words[13] = load32(block + 4 * 13);
block_words[14] = load32(block + 4 * 14);
block_words[15] = load32(block + 4 * 15);
state[0] = cv[0];
state[1] = cv[1];
state[2] = cv[2];
state[3] = cv[3];
state[4] = cv[4];
state[5] = cv[5];
state[6] = cv[6];
state[7] = cv[7];
state[8] = IV[0];
state[9] = IV[1];
state[10] = IV[2];
state[11] = IV[3];
state[12] = counter_low(counter);
state[13] = counter_high(counter);
state[14] = (uint32_t)block_len;
state[15] = (uint32_t)flags;
round_fn(state, &block_words[0], 0);
round_fn(state, &block_words[0], 1);
round_fn(state, &block_words[0], 2);
round_fn(state, &block_words[0], 3);
round_fn(state, &block_words[0], 4);
round_fn(state, &block_words[0], 5);
round_fn(state, &block_words[0], 6);
}
void blake3_compress_in_place_portable(uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter,
uint8_t flags) {
uint32_t state[16];
compress_pre(state, cv, block, block_len, counter, flags);
cv[0] = state[0] ^ state[8];
cv[1] = state[1] ^ state[9];
cv[2] = state[2] ^ state[10];
cv[3] = state[3] ^ state[11];
cv[4] = state[4] ^ state[12];
cv[5] = state[5] ^ state[13];
cv[6] = state[6] ^ state[14];
cv[7] = state[7] ^ state[15];
}
void blake3_compress_xof_portable(const uint32_t cv[8],
const uint8_t block[BLAKE3_BLOCK_LEN],
uint8_t block_len, uint64_t counter,
uint8_t flags, uint8_t out[64]) {
uint32_t state[16];
compress_pre(state, cv, block, block_len, counter, flags);
store32(&out[0 * 4], state[0] ^ state[8]);
store32(&out[1 * 4], state[1] ^ state[9]);
store32(&out[2 * 4], state[2] ^ state[10]);
store32(&out[3 * 4], state[3] ^ state[11]);
store32(&out[4 * 4], state[4] ^ state[12]);
store32(&out[5 * 4], state[5] ^ state[13]);
store32(&out[6 * 4], state[6] ^ state[14]);
store32(&out[7 * 4], state[7] ^ state[15]);
store32(&out[8 * 4], state[8] ^ cv[0]);
store32(&out[9 * 4], state[9] ^ cv[1]);
store32(&out[10 * 4], state[10] ^ cv[2]);
store32(&out[11 * 4], state[11] ^ cv[3]);
store32(&out[12 * 4], state[12] ^ cv[4]);
store32(&out[13 * 4], state[13] ^ cv[5]);
store32(&out[14 * 4], state[14] ^ cv[6]);
store32(&out[15 * 4], state[15] ^ cv[7]);
}
INLINE void hash_one_portable(const uint8_t *input, size_t blocks,
const uint32_t key[8], uint64_t counter,
uint8_t flags, uint8_t flags_start,
uint8_t flags_end, uint8_t out[BLAKE3_OUT_LEN]) {
uint32_t cv[8];
memcpy(cv, key, BLAKE3_KEY_LEN);
uint8_t block_flags = flags | flags_start;
while (blocks > 0) {
if (blocks == 1) {
block_flags |= flags_end;
}
blake3_compress_in_place_portable(cv, input, BLAKE3_BLOCK_LEN, counter,
block_flags);
input = &input[BLAKE3_BLOCK_LEN];
blocks -= 1;
block_flags = flags;
}
store_cv_words(out, cv);
}
void blake3_hash_many_portable(const uint8_t *const *inputs, size_t num_inputs,
size_t blocks, const uint32_t key[8],
uint64_t counter, bool increment_counter,
uint8_t flags, uint8_t flags_start,
uint8_t flags_end, uint8_t *out) {
while (num_inputs > 0) {
hash_one_portable(inputs[0], blocks, key, counter, flags, flags_start,
flags_end, out);
if (increment_counter) {
counter += 1;
}
inputs += 1;
num_inputs -= 1;
out = &out[BLAKE3_OUT_LEN];
}
}
+2 -1
View File
@@ -75,7 +75,8 @@ enum {
GETOPT_VAL_TCP_INCOMING_RCVBUF,
GETOPT_VAL_TCP_OUTGOING_SNDBUF,
GETOPT_VAL_TCP_OUTGOING_RCVBUF,
GETOPT_VAL_NFTABLES_SETS
GETOPT_VAL_NFTABLES_SETS,
GETOPT_VAL_SERVER_URL
};
#endif // _COMMON_H
+68 -13
View File
@@ -192,6 +192,11 @@ crypto_init(const char *password, const char *key, const char *method)
.decrypt = &aead_decrypt,
.ctx_init = &aead_ctx_init,
.ctx_release = &aead_ctx_release,
/* AEAD-2022 replaces the stateless UDP path with sessions */
.encrypt_udp = aead_is_2022(cipher) ? &aead_2022_encrypt_udp : NULL,
.decrypt_udp = aead_is_2022(cipher) ? &aead_2022_decrypt_udp : NULL,
.udp_session_release = aead_is_2022(cipher)
? &aead_2022_udp_session_release : NULL,
};
memcpy(crypto, &tmp, sizeof(crypto_t));
return crypto;
@@ -354,15 +359,40 @@ crypto_hkdf_expand(const mbedtls_md_info_t *md, const unsigned char *prk,
return 0;
}
/*
* Report an unusable key and exit, after showing a freshly generated one of
* the right size so the user has something to paste back.
*/
static void
crypto_key_fatal(const char *what, uint8_t *key, size_t key_len)
{
char encoded[BASE64_SIZE(MAX_KEY_LENGTH)];
rand_bytes(key, key_len);
base64_encode(encoded, sizeof(encoded), key, key_len);
LOGE("Invalid %s for your chosen cipher!", what);
LOGE("It requires a " SIZE_FMT "-byte key encoded with Base64", key_len);
LOGE("Generating a new random key: %s", encoded);
FATAL("Please use the key above or input a valid key");
}
int
crypto_parse_key(const char *base64, uint8_t *key, size_t key_len)
{
size_t base64_len = strlen(base64);
int out_len = BASE64_SIZE(base64_len);
uint8_t out[out_len];
/*
* Decode into a fixed buffer rather than one sized from the input: the
* key comes from the command line or config file, and a VLA sized by
* its length lets a long string overflow the stack. Only the first
* key_len bytes are ever used, and base64_decode never writes past the
* size it is given.
*/
uint8_t out[MAX_KEY_LENGTH];
out_len = base64_decode(out, base64, out_len);
if (out_len > 0 && out_len >= key_len) {
if (key_len > sizeof(out))
FATAL("Key length exceeds the maximum supported size");
int out_len = base64_decode(out, base64, (int)key_len);
if (out_len > 0 && (size_t)out_len >= key_len) {
memcpy(key, out, key_len);
#ifdef SS_DEBUG
dump("KEY", (char *)key, key_len);
@@ -370,14 +400,39 @@ crypto_parse_key(const char *base64, uint8_t *key, size_t key_len)
return key_len;
}
out_len = BASE64_SIZE(key_len);
char out_key[out_len];
rand_bytes(key, key_len);
base64_encode(out_key, out_len, key, key_len);
LOGE("Invalid key for your chosen cipher!");
LOGE("It requires a " SIZE_FMT "-byte key encoded with URL-safe Base64", key_len);
LOGE("Generating a new random key: %s", out_key);
FATAL("Please use the key above or input a valid key");
crypto_key_fatal("key", key, key_len);
return key_len;
}
/*
* SIP022 requires a base64-encoded pre-shared key of exactly the cipher's
* key size. Unlike crypto_parse_key there is no password fallback: a wrong
* size is a hard error, since deriving a key from a password is forbidden.
*/
int
crypto_parse_psk(const char *base64, uint8_t *key, size_t key_len)
{
/*
* One byte of slack lets an over-long key be told apart from an exact
* one: base64_decode stops at the size it is given, so decoding into
* key_len + 1 bytes yields key_len only for a key of exactly that size.
*/
uint8_t out[MAX_KEY_LENGTH + 1];
if (key_len >= sizeof(out))
FATAL("Key length exceeds the maximum supported size");
int out_len = base64_decode(out, base64, (int)key_len + 1);
if (out_len != (int)key_len) {
LOGE("AEAD-2022 requires exactly a " SIZE_FMT "-byte key in base64", key_len);
crypto_key_fatal("pre-shared key", key, key_len);
}
memcpy(key, out, key_len);
#ifdef SS_DEBUG
dump("PSK", (char *)key, key_len);
#endif
return key_len;
}
+38 -1
View File
@@ -71,6 +71,16 @@ typedef mbedtls_md_info_t digest_type_t;
#define SUBKEY_INFO "ss-subkey"
#define IV_INFO "ss-iv"
/* SIP022 AEAD-2022 (Shadowsocks 2022 Edition) */
#define SS2022_SUBKEY_CTX "shadowsocks 2022 session subkey"
#define SS2022_TIME_WINDOW 30
#define SS2022_SALT_RETENTION 60
#define SS2022_MAX_PADDING 900
#define SS2022_SESSION_ID_LEN 8
#define CRYPTO_ROLE_CLIENT 0
#define CRYPTO_ROLE_SERVER 1
#ifndef BF_NUM_ENTRIES_FOR_SERVER
#define BF_NUM_ENTRIES_FOR_SERVER 1e6
#endif
@@ -103,7 +113,7 @@ typedef struct {
uint8_t key[MAX_KEY_LENGTH];
} cipher_t;
typedef struct {
typedef struct cipher_ctx {
uint32_t init;
uint64_t counter;
cipher_evp_t *evp;
@@ -113,6 +123,13 @@ typedef struct {
uint8_t salt[MAX_KEY_LENGTH];
uint8_t skey[MAX_KEY_LENGTH];
uint8_t nonce[MAX_NONCE_LENGTH];
/* AEAD-2022 state */
struct cipher_ctx *peer; /* paired enc/dec ctx of the same connection */
uint8_t role; /* CRYPTO_ROLE_CLIENT or CRYPTO_ROLE_SERVER */
uint8_t hdr_stage; /* TCP decrypt header state machine stage */
uint16_t hdr_varlen; /* pending request variable-length header size */
uint16_t next_plen; /* pending response first payload chunk size */
} cipher_ctx_t;
typedef struct crypto {
@@ -125,8 +142,27 @@ typedef struct crypto {
void(*const ctx_init) (cipher_t *, cipher_ctx_t *, int);
void(*const ctx_release) (cipher_ctx_t *);
/*
* AEAD-2022 session-based UDP. NULL for legacy ciphers; when set,
* udprelay must use these instead of encrypt_all/decrypt_all, passing
* a per-conversation session slot. The slot is released with
* udp_session_release.
*/
int(*const encrypt_udp) (buffer_t *, cipher_t *, size_t, void **);
int(*const decrypt_udp) (buffer_t *, cipher_t *, size_t, void **);
void(*const udp_session_release) (void *);
} crypto_t;
/* Pair the encrypt/decrypt contexts of one connection (needed by AEAD-2022
* to echo and verify the request salt in the response header). */
static inline void
cipher_ctx_pair(cipher_ctx_t *a, cipher_ctx_t *b)
{
a->peer = b;
b->peer = a;
}
int balloc(buffer_t *, size_t);
int brealloc(buffer_t *, size_t, size_t);
int bprepend(buffer_t *, buffer_t *, size_t);
@@ -150,6 +186,7 @@ unsigned char *crypto_md5(const unsigned char *, size_t, unsigned char *);
int crypto_derive_key(const char *, uint8_t *, size_t);
int crypto_parse_key(const char *, uint8_t *, size_t);
int crypto_parse_psk(const char *, uint8_t *, size_t);
int crypto_hkdf(const mbedtls_md_info_t *md, const unsigned char *salt,
int salt_len, const unsigned char *ikm, int ikm_len,
const unsigned char *info, int info_len, unsigned char *okm,
+35 -5
View File
@@ -52,6 +52,7 @@
#include <libcork/core.h>
#include "netutils.h"
#include "ssurl.h"
#include "utils.h"
#include "socks5.h"
#include "acl.h"
@@ -396,7 +397,7 @@ server_handshake(EV_P_ ev_io *w, buffer_t *buf)
LOGI("inet_ntop(AF_INET): %s", strerror(errno));
ip[0] = '\0';
}
sprintf(port, "%d", p);
snprintf(port, sizeof(port), "%d", p);
}
} else if (atyp == SOCKS5_ATYP_DOMAIN) {
if (buf->len < request_len + 1) {
@@ -414,7 +415,7 @@ server_handshake(EV_P_ ev_io *w, buffer_t *buf)
uint16_t p = load16_be(buf->data + request_len + 1 + name_len);
memcpy(host, buf->data + request_len + 1, name_len);
host[name_len] = '\0';
sprintf(port, "%d", p);
snprintf(port, sizeof(port), "%d", p);
}
} else if (atyp == SOCKS5_ATYP_IPV6) {
size_t in6_addr_len = sizeof(struct in6_addr);
@@ -431,7 +432,7 @@ server_handshake(EV_P_ ev_io *w, buffer_t *buf)
LOGI("inet_ntop(AF_INET6): %s", strerror(errno));
ip[0] = '\0';
}
sprintf(port, "%d", p);
snprintf(port, sizeof(port), "%d", p);
}
} else {
LOGE("unsupported addrtype: %d", request->atyp);
@@ -1217,6 +1218,7 @@ new_server(int fd)
server->d_ctx = ss_malloc(sizeof(cipher_ctx_t));
crypto->ctx_init(crypto->cipher, server->e_ctx, 1);
crypto->ctx_init(crypto->cipher, server->d_ctx, 0);
cipher_ctx_pair(server->e_ctx, server->d_ctx);
ev_io_init(&server->recv_ctx->io, server_recv_cb, fd, EV_READ);
ev_io_init(&server->send_ctx->io, server_send_cb, fd, EV_WRITE);
@@ -1454,6 +1456,9 @@ main(int argc, char **argv)
ss_addr_t remote_addr[MAX_REMOTE_NUM];
char *remote_port = NULL;
/* Lives until exit; its strings are handed to the config above. */
ss_url_t server_url = { 0 };
memset(remote_addr, 0, sizeof(ss_addr_t) * MAX_REMOTE_NUM);
static struct option long_options[] = {
@@ -1471,6 +1476,7 @@ main(int argc, char **argv)
{ "plugin-opts", required_argument, NULL, GETOPT_VAL_PLUGIN_OPTS },
{ "password", required_argument, NULL, GETOPT_VAL_PASSWORD },
{ "key", required_argument, NULL, GETOPT_VAL_KEY },
{ "server-url", required_argument, NULL, GETOPT_VAL_SERVER_URL },
{ "help", no_argument, NULL, GETOPT_VAL_HELP },
{ NULL, 0, NULL, 0 }
};
@@ -1518,6 +1524,30 @@ main(int argc, char **argv)
case GETOPT_VAL_KEY:
key = optarg;
break;
case GETOPT_VAL_SERVER_URL:
/*
* An ss:// URI carries the server, credentials and plugin in one
* argument. Options given later on the command line still win,
* since they are applied as getopt reaches them.
*/
if (ss_url_parse(optarg, &server_url) != 0) {
FATAL("invalid ss:// server URL");
}
if (remote_num < MAX_REMOTE_NUM) {
remote_addr[remote_num].host = server_url.host;
remote_addr[remote_num].port = NULL;
remote_num++;
}
remote_port = server_url.port;
method = server_url.method;
password = server_url.password;
if (server_url.plugin != NULL)
plugin = server_url.plugin;
if (server_url.plugin_opts != NULL)
plugin_opts = server_url.plugin_opts;
if (server_url.tag != NULL)
LOGI("using server \"%s\"", server_url.tag);
break;
case GETOPT_VAL_REUSE_PORT:
reuse_port = 1;
break;
@@ -2078,8 +2108,8 @@ _start_ss_local_server(profile_t profile, ss_local_callback callback, void *udat
char local_port_str[16];
char remote_port_str[16];
sprintf(local_port_str, "%d", local_port);
sprintf(remote_port_str, "%d", remote_port);
snprintf(local_port_str, sizeof(local_port_str), "%d", local_port);
snprintf(remote_port_str, sizeof(remote_port_str), "%d", remote_port);
#ifdef __MINGW32__
winsock_init();
+1
View File
@@ -694,6 +694,7 @@ new_server(int fd)
server->d_ctx = ss_malloc(sizeof(cipher_ctx_t));
crypto->ctx_init(crypto->cipher, server->e_ctx, 1);
crypto->ctx_init(crypto->cipher, server->d_ctx, 0);
cipher_ctx_pair(server->e_ctx, server->d_ctx);
ev_io_init(&server->recv_ctx->io, server_recv_cb, fd, EV_READ);
ev_io_init(&server->send_ctx->io, server_send_cb, fd, EV_WRITE);
+1
View File
@@ -1650,6 +1650,7 @@ new_server(int fd, listen_ctx_t *listener)
server->d_ctx = ss_malloc(sizeof(cipher_ctx_t));
crypto->ctx_init(crypto->cipher, server->e_ctx, 1);
crypto->ctx_init(crypto->cipher, server->d_ctx, 0);
cipher_ctx_pair(server->e_ctx, server->d_ctx);
int timeout = max(MIN_TCP_IDLE_TIMEOUT, server->listen_ctx->timeout);
ev_io_init(&server->recv_ctx->io, server_recv_cb, fd, EV_READ);
+320
View File
@@ -0,0 +1,320 @@
/*
* ssurl.c - Parse ss:// server URIs (SIP002 and the legacy form)
*
* Copyright (C) 2013 - 2026, Max Lv <max.c.lv@gmail.com>
*
* This file is part of the shadowsocks-libev.
*
* shadowsocks-libev is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* shadowsocks-libev is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with shadowsocks-libev; see the file COPYING. If not, see
* <http://www.gnu.org/licenses/>.
*
* Spec: https://shadowsocks.org/guide/sip002.html
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <stdlib.h>
#include <string.h>
#include <strings.h>
#include "base64.h"
#include "ssurl.h"
#include "utils.h"
#define SS_URL_SCHEME "ss://"
/* memrchr is a GNU extension and is absent on macOS and the BSDs. */
static const char *
find_last(const char *s, size_t len, char c)
{
for (size_t i = len; i > 0; i--) {
if (s[i - 1] == c)
return s + i - 1;
}
return NULL;
}
static char *
str_ndup(const char *s, size_t n)
{
char *out = ss_malloc(n + 1);
memcpy(out, s, n);
out[n] = '\0';
return out;
}
static int
hex_value(char c)
{
if (c >= '0' && c <= '9')
return c - '0';
if (c >= 'a' && c <= 'f')
return c - 'a' + 10;
if (c >= 'A' && c <= 'F')
return c - 'A' + 10;
return -1;
}
/* Percent-decode in place semantics: returns a newly allocated string. */
static char *
percent_decode(const char *s, size_t len)
{
char *out = ss_malloc(len + 1);
size_t o = 0;
for (size_t i = 0; i < len; i++) {
if (s[i] == '%' && i + 2 < len) {
int hi = hex_value(s[i + 1]);
int lo = hex_value(s[i + 2]);
if (hi >= 0 && lo >= 0) {
out[o++] = (char)((hi << 4) | lo);
i += 2;
continue;
}
}
out[o++] = s[i];
}
out[o] = '\0';
return out;
}
/*
* Base64-decode into a fresh string. Returns NULL if the input is not
* valid base64. base64_decode accepts both the standard and URL-safe
* alphabets and stops at padding, so unpadded input works too.
*/
static char *
base64_dup(const char *s, size_t len)
{
if (len == 0)
return NULL;
char *in = str_ndup(s, len);
/* decoded output is always shorter than the encoded input */
uint8_t *out = ss_malloc(len + 1);
int out_len = base64_decode(out, in, (int)len);
ss_free(in);
if (out_len <= 0) {
ss_free(out);
return NULL;
}
out[out_len] = '\0';
return (char *)out;
}
/*
* Split "host:port" and store the parts. Handles the bracketed IPv6 form
* [::1]:8388. Returns 0 on success.
*/
static int
split_host_port(const char *s, size_t len, ss_url_t *url)
{
if (len == 0)
return -1;
if (s[0] == '[') {
const char *close = memchr(s, ']', len);
if (close == NULL)
return -1;
size_t host_len = (size_t)(close - s) - 1;
if (host_len == 0)
return -1;
size_t rest = len - (size_t)(close - s) - 1;
if (rest < 2 || close[1] != ':')
return -1;
url->host = str_ndup(s + 1, host_len);
url->port = str_ndup(close + 2, rest - 1);
return 0;
}
const char *colon = find_last(s, len, ':');
if (colon == NULL || colon == s)
return -1;
size_t host_len = (size_t)(colon - s);
size_t port_len = len - host_len - 1;
if (port_len == 0)
return -1;
url->host = str_ndup(s, host_len);
url->port = str_ndup(colon + 1, port_len);
return 0;
}
/* Split "method:password" into the url fields. Returns 0 on success. */
static int
split_userinfo(const char *s, ss_url_t *url)
{
const char *colon = strchr(s, ':');
if (colon == NULL || colon == s || colon[1] == '\0')
return -1;
url->method = str_ndup(s, (size_t)(colon - s));
url->password = str_ndup(colon + 1, strlen(colon + 1));
return 0;
}
/*
* Read the query string. Only "plugin" is meaningful to us; per SIP003 its
* value is the plugin name, optionally followed by ';' and the options.
*/
static void
parse_query(const char *s, size_t len, ss_url_t *url)
{
const char *end = s + len;
while (s < end) {
const char *amp = memchr(s, '&', (size_t)(end - s));
const char *pair_end = amp ? amp : end;
const char *eq = memchr(s, '=', (size_t)(pair_end - s));
if (eq != NULL) {
size_t name_len = (size_t)(eq - s);
if (name_len == 6 && strncmp(s, "plugin", 6) == 0) {
char *value = percent_decode(eq + 1, (size_t)(pair_end - eq - 1));
char *semi = strchr(value, ';');
if (semi != NULL) {
*semi = '\0';
url->plugin_opts = str_ndup(semi + 1, strlen(semi + 1));
}
url->plugin = value;
}
}
if (amp == NULL)
break;
s = amp + 1;
}
}
void
ss_url_free(ss_url_t *url)
{
if (url == NULL)
return;
ss_free(url->method);
ss_free(url->password);
ss_free(url->host);
ss_free(url->port);
ss_free(url->plugin);
ss_free(url->plugin_opts);
ss_free(url->tag);
memset(url, 0, sizeof(*url));
}
int
ss_url_parse(const char *uri, ss_url_t *url)
{
if (uri == NULL || url == NULL)
return -1;
memset(url, 0, sizeof(*url));
if (strncasecmp(uri, SS_URL_SCHEME, strlen(SS_URL_SCHEME)) != 0)
return -1;
const char *body = uri + strlen(SS_URL_SCHEME);
size_t body_len = strlen(body);
if (body_len == 0)
return -1;
/* The fragment is a human-readable tag and is not part of the config. */
const char *hash = memchr(body, '#', body_len);
if (hash != NULL) {
url->tag = percent_decode(hash + 1, body_len - (size_t)(hash - body) - 1);
body_len = (size_t)(hash - body);
}
const char *query = memchr(body, '?', body_len);
size_t head_len = query ? (size_t)(query - body) : body_len;
/*
* SIP002 keeps the host in the clear, so an '@' separates the encoded
* userinfo from it. Without one this is the legacy form, where the
* whole body is a single base64 blob.
*/
const char *at = find_last(body, head_len, '@');
if (at != NULL) {
size_t userinfo_len = (size_t)(at - body);
if (userinfo_len == 0)
goto fail;
/*
* The userinfo is normally base64, but some producers write
* "method:password" directly; base64 decoding fails on the ':'
* so a failed decode simply means the plain form.
*/
char *userinfo = base64_dup(body, userinfo_len);
if (userinfo == NULL || strchr(userinfo, ':') == NULL) {
ss_free(userinfo);
userinfo = percent_decode(body, userinfo_len);
}
int rc = split_userinfo(userinfo, url);
ss_free(userinfo);
if (rc != 0)
goto fail;
/* A trailing '/' before the query or end is allowed by SIP002. */
size_t hostport_len = head_len - userinfo_len - 1;
const char *hostport = at + 1;
if (hostport_len > 0 && hostport[hostport_len - 1] == '/')
hostport_len--;
if (split_host_port(hostport, hostport_len, url) != 0)
goto fail;
} else {
char *plain = base64_dup(body, head_len);
if (plain == NULL)
goto fail;
char *sep = strrchr(plain, '@');
if (sep == NULL) {
ss_free(plain);
goto fail;
}
*sep = '\0';
int rc = split_userinfo(plain, url);
if (rc == 0)
rc = split_host_port(sep + 1, strlen(sep + 1), url);
ss_free(plain);
if (rc != 0)
goto fail;
}
if (query != NULL) {
size_t query_ofst = (size_t)(query - body) + 1;
parse_query(body + query_ofst, body_len - query_ofst, url);
}
return 0;
fail:
ss_url_free(url);
return -1;
}
+58
View File
@@ -0,0 +1,58 @@
/*
* ssurl.h - Parse ss:// server URIs (SIP002 and the legacy form)
*
* Copyright (C) 2013 - 2026, Max Lv <max.c.lv@gmail.com>
*
* This file is part of the shadowsocks-libev.
*
* shadowsocks-libev is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 3 of the License, or
* (at your option) any later version.
*
* shadowsocks-libev is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with shadowsocks-libev; see the file COPYING. If not, see
* <http://www.gnu.org/licenses/>.
*/
#ifndef _SSURL_H
#define _SSURL_H
/*
* A parsed ss:// URI. Every field is heap-allocated and owned by the
* struct; release them all with ss_url_free(). Fields that the URI did not
* carry are NULL.
*/
typedef struct ss_url {
char *method;
char *password;
char *host;
char *port;
char *plugin;
char *plugin_opts;
char *tag;
} ss_url_t;
/*
* Parse an ss:// URI in either the SIP002 form
*
* ss://base64url(method:password)@host:port/?plugin=name;opts#tag
*
* or the legacy form
*
* ss://base64(method:password@host:port)#tag
*
* Returns 0 on success, -1 if the URI is malformed. On failure nothing is
* allocated and *url is left zeroed.
*/
int ss_url_parse(const char *uri, ss_url_t *url);
/* Free every field and zero the struct. Safe on a zeroed struct. */
void ss_url_free(ss_url_t *url);
#endif // _SSURL_H
+1
View File
@@ -717,6 +717,7 @@ new_server(int fd)
server->d_ctx = ss_malloc(sizeof(cipher_ctx_t));
crypto->ctx_init(crypto->cipher, server->e_ctx, 1);
crypto->ctx_init(crypto->cipher, server->d_ctx, 0);
cipher_ctx_pair(server->e_ctx, server->d_ctx);
ev_io_init(&server->recv_ctx->io, server_recv_cb, fd, EV_READ);
ev_io_init(&server->send_ctx->io, server_send_cb, fd, EV_WRITE);
+35 -7
View File
@@ -328,7 +328,7 @@ parse_udprelay_header(const char *buf, const size_t buf_len,
}
if (port != NULL) {
sprintf(port, "%d", load16_be(buf + offset));
snprintf(port, MAX_PORT_STR_LEN, "%d", load16_be(buf + offset));
}
offset += 2;
@@ -351,14 +351,14 @@ get_addr_str(const struct sockaddr *sa, bool has_port)
memcpy(&sa_in, sa, sizeof(struct sockaddr_in));
inet_ntop(AF_INET, &sa_in.sin_addr, addr, INET_ADDRSTRLEN);
p = ntohs(sa_in.sin_port);
sprintf(port, "%d", p);
snprintf(port, sizeof(port), "%d", p);
break;
case AF_INET6:
memcpy(&sa_in6, sa, sizeof(struct sockaddr_in6));
inet_ntop(AF_INET6, &sa_in6.sin6_addr, addr, INET6_ADDRSTRLEN);
p = ntohs(sa_in6.sin6_port);
sprintf(port, "%d", p);
snprintf(port, sizeof(port), "%d", p);
break;
default:
@@ -638,6 +638,11 @@ close_and_free_remote(EV_P_ remote_ctx_t *ctx)
ev_timer_stop(EV_A_ & ctx->watcher);
ev_io_stop(EV_A_ & ctx->io);
close(ctx->fd);
if (ctx->udp_session != NULL
&& ctx->server_ctx != NULL
&& ctx->server_ctx->crypto->udp_session_release != NULL) {
ctx->server_ctx->crypto->udp_session_release(ctx->udp_session);
}
ss_free(ctx);
}
}
@@ -821,7 +826,10 @@ remote_recv_cb(EV_P_ ev_io *w, int revents)
buf->len = r;
#ifdef MODULE_LOCAL
int err = server_ctx->crypto->decrypt_all(buf, server_ctx->crypto->cipher, buf_size);
int err = server_ctx->crypto->decrypt_udp != NULL
? server_ctx->crypto->decrypt_udp(buf, server_ctx->crypto->cipher,
buf_size, &remote_ctx->udp_session)
: server_ctx->crypto->decrypt_all(buf, server_ctx->crypto->cipher, buf_size);
if (err) {
LOGE("failed to handshake with %s: %s",
get_addr_str((struct sockaddr *)&src_addr, false), "suspicious UDP packet");
@@ -880,7 +888,10 @@ remote_recv_cb(EV_P_ ev_io *w, int revents)
memcpy(buf->data, addr_header, addr_header_len);
buf->len += addr_header_len;
int err = server_ctx->crypto->encrypt_all(buf, server_ctx->crypto->cipher, buf_size);
int err = server_ctx->crypto->encrypt_udp != NULL
? server_ctx->crypto->encrypt_udp(buf, server_ctx->crypto->cipher,
buf_size, &remote_ctx->udp_session)
: server_ctx->crypto->encrypt_all(buf, server_ctx->crypto->cipher, buf_size);
if (err) {
// drop the packet silently
goto CLEAN_UP;
@@ -1050,7 +1061,16 @@ server_recv_cb(EV_P_ ev_io *w, int revents)
#ifdef MODULE_REMOTE
tx += buf->len;
int err = server_ctx->crypto->decrypt_all(buf, server_ctx->crypto->cipher, buf_size);
/*
* AEAD-2022 routes by client session ID, which is only known after
* decryption, so the crypto layer resolves the session itself and hands
* it back here to be attached to the relay context.
*/
void *udp_session = NULL;
int err = server_ctx->crypto->decrypt_udp != NULL
? server_ctx->crypto->decrypt_udp(buf, server_ctx->crypto->cipher,
buf_size, &udp_session)
: server_ctx->crypto->decrypt_all(buf, server_ctx->crypto->cipher, buf_size);
if (err) {
LOGE("failed to handshake with %s: %s",
get_addr_str((struct sockaddr *)&src_addr, false), "suspicious UDP packet");
@@ -1331,7 +1351,10 @@ server_recv_cb(EV_P_ ev_io *w, int revents)
memmove(buf->data, buf->data + offset, buf->len);
}
int err = server_ctx->crypto->encrypt_all(buf, server_ctx->crypto->cipher, buf_size);
int err = server_ctx->crypto->encrypt_udp != NULL
? server_ctx->crypto->encrypt_udp(buf, server_ctx->crypto->cipher,
buf_size, &remote_ctx->udp_session)
: server_ctx->crypto->encrypt_all(buf, server_ctx->crypto->cipher, buf_size);
if (err) {
// drop the packet silently
@@ -1411,6 +1434,11 @@ server_recv_cb(EV_P_ ev_io *w, int revents)
}
}
if (remote_ctx != NULL) {
// Attach the AEAD-2022 session so replies are encrypted with it
remote_ctx->udp_session = udp_session;
}
if (remote_ctx != NULL && !need_query) {
size_t addr_len = get_sockaddr_len((struct sockaddr *)&dst_addr);
int s = sendto(remote_ctx->fd, buf->data + addr_header_len,
+6
View File
@@ -87,6 +87,12 @@ typedef struct remote_ctx {
#ifdef MODULE_REMOTE
struct sockaddr_storage dst_addr;
#endif
/*
* AEAD-2022 UDP session state for this conversation, owned by the
* crypto layer and released with crypto->udp_session_release.
* NULL for legacy ciphers.
*/
void *udp_session;
struct server_ctx *server_ctx;
} remote_ctx_t;
+6
View File
@@ -461,6 +461,12 @@ usage()
" [--no-delay] Enable TCP_NODELAY.\n");
printf(
" [--key <key_in_base64>] Key of your remote server.\n");
#endif
#ifdef MODULE_LOCAL
printf(
" [--server-url <ss_url>] Server as an ss:// URL, carrying the\n");
printf(
" address, cipher, password and plugin.\n");
#endif
printf(
" [--plugin <name>] Enable SIP003 plugin. (Experimental)\n");
+10
View File
@@ -0,0 +1,10 @@
{
"server": "127.0.0.1",
"server_port": 8389,
"local_port": 1081,
"password": "AAECAwQFBgcICQoLDA0ODw==",
"timeout": 60,
"method": "2022-blake3-aes-128-gcm",
"local": "127.0.0.1",
"fast_open": false
}
+10
View File
@@ -0,0 +1,10 @@
{
"server": "127.0.0.1",
"server_port": 8389,
"local_port": 1081,
"password": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
"timeout": 60,
"method": "2022-blake3-aes-256-gcm",
"local": "127.0.0.1",
"fast_open": false
}
+10
View File
@@ -0,0 +1,10 @@
{
"server": "127.0.0.1",
"server_port": 8389,
"local_port": 1081,
"password": "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
"timeout": 60,
"method": "2022-blake3-chacha20-poly1305",
"local": "127.0.0.1",
"fast_open": false
}
+40 -3
View File
@@ -60,6 +60,11 @@ ss_add_test(test_json
"test_json.c;${PROJECT_SOURCE_DIR}/src/json.c"
"m")
# test_ssurl - ss:// URI parsing (SIP002 and legacy)
ss_add_test(test_ssurl
"test_ssurl.c;${PROJECT_SOURCE_DIR}/src/ssurl.c;${PROJECT_SOURCE_DIR}/src/base64.c;${PROJECT_SOURCE_DIR}/src/utils.c"
"${LIBSODIUM_SHARED}")
# test_netutils - needs cork for ip address parsing
set(TEST_NETUTILS_LIBS
${LIBSODIUM_SHARED}
@@ -131,10 +136,25 @@ ss_add_test(test_jconf
"test_jconf.c;${PROJECT_SOURCE_DIR}/src/jconf.c;${PROJECT_SOURCE_DIR}/src/json.c;${PROJECT_SOURCE_DIR}/src/utils.c;${PROJECT_SOURCE_DIR}/src/netutils.c"
"${TEST_JCONF_LIBS};m")
# Crypto sources shared by the tests that link the crypto layer. The AEAD-2022
# ciphers pull in BLAKE3 (linked as the `blake3` target) and the session-based
# UDP codec, so they must be built together with crypto.c.
set(SS_CRYPTO_TEST_SOURCES
${PROJECT_SOURCE_DIR}/src/crypto.c
${PROJECT_SOURCE_DIR}/src/aead.c
${PROJECT_SOURCE_DIR}/src/aead2022.c
${PROJECT_SOURCE_DIR}/src/aead2022_udp.c
${PROJECT_SOURCE_DIR}/src/stream.c
${PROJECT_SOURCE_DIR}/src/ppbloom.c
${PROJECT_SOURCE_DIR}/src/cache.c
${PROJECT_SOURCE_DIR}/src/base64.c
${PROJECT_SOURCE_DIR}/src/utils.c
)
# test_buffer - needs sodium and mbedtls for crypto.c buffer functions
ss_add_test(test_buffer
"test_buffer.c;${PROJECT_SOURCE_DIR}/src/crypto.c;${PROJECT_SOURCE_DIR}/src/utils.c;${PROJECT_SOURCE_DIR}/src/base64.c;${PROJECT_SOURCE_DIR}/src/aead.c;${PROJECT_SOURCE_DIR}/src/stream.c;${PROJECT_SOURCE_DIR}/src/ppbloom.c;${PROJECT_SOURCE_DIR}/src/cache.c"
"${LIBSODIUM_SHARED};${LIBMBEDTLS_SHARED};${LIBMBEDCRYPTO_SHARED};${LIBEV_SHARED};${TEST_PPBLOOM_LIBS};m")
"test_buffer.c;${SS_CRYPTO_TEST_SOURCES}"
"${LIBSODIUM_SHARED};${LIBMBEDTLS_SHARED};${LIBMBEDCRYPTO_SHARED};${LIBEV_SHARED};${TEST_PPBLOOM_LIBS};blake3;m")
# test_crypto - full crypto test
set(TEST_CRYPTO_LIBS
@@ -148,10 +168,27 @@ if(WITH_EMBEDDED_SRC)
else()
list(APPEND TEST_CRYPTO_LIBS ${LIBBLOOM_SHARED})
endif()
# BLAKE3 backs the 2022 ciphers' key derivation
list(APPEND TEST_CRYPTO_LIBS blake3)
ss_add_test(test_crypto
"test_crypto.c;${PROJECT_SOURCE_DIR}/src/crypto.c;${PROJECT_SOURCE_DIR}/src/aead.c;${PROJECT_SOURCE_DIR}/src/stream.c;${PROJECT_SOURCE_DIR}/src/ppbloom.c;${PROJECT_SOURCE_DIR}/src/cache.c;${PROJECT_SOURCE_DIR}/src/base64.c;${PROJECT_SOURCE_DIR}/src/utils.c"
"test_crypto.c;${SS_CRYPTO_TEST_SOURCES}"
"${TEST_CRYPTO_LIBS};m")
# test_aead2022 - Shadowsocks 2022 Edition (SIP022) ciphers
ss_add_test(test_aead2022
"test_aead2022.c;${SS_CRYPTO_TEST_SOURCES}"
"${TEST_CRYPTO_LIBS};m")
# test_interop_rust - interoperability against shadowsocks-rust.
# Skips itself when shadowsocks-rust or the built binaries are absent.
add_test(NAME test_interop_rust
COMMAND bash "${PROJECT_SOURCE_DIR}/tests/test_interop_rust.sh"
WORKING_DIRECTORY "${PROJECT_SOURCE_DIR}")
set_tests_properties(test_interop_rust PROPERTIES
LABELS "integration"
TIMEOUT 300
ENVIRONMENT "SS_BIN_DIR=${RUNTIME_SHARED_OUTPUT_DIRECTORY}")
# test_ss_setup - bash unit tests for the ss-setup TUI tool
add_test(NAME test_ss_setup
COMMAND bash "${PROJECT_SOURCE_DIR}/tests/test_ss_setup.sh"
+3
View File
@@ -40,5 +40,8 @@ run_test python tests/test.py $BIN -c tests/salsa20.json
run_test python tests/test.py $BIN -c tests/chacha20.json
run_test python tests/test.py $BIN -c tests/chacha20-ietf.json
run_test python tests/test.py $BIN -c tests/chacha20-ietf-poly1305.json
run_test python tests/test.py $BIN -c tests/2022-blake3-aes-128-gcm.json
run_test python tests/test.py $BIN -c tests/2022-blake3-aes-256-gcm.json
run_test python tests/test.py $BIN -c tests/2022-blake3-chacha20-poly1305.json
exit $result
+392
View File
@@ -0,0 +1,392 @@
/*
* Unit tests for the Shadowsocks 2022 Edition (SIP022) cipher suite.
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <assert.h>
#include <string.h>
#include <stdlib.h>
#include <sodium.h>
int verbose = 0;
#include "crypto.h"
#include "aead.h"
#include "ppbloom.h"
#include "utils.h"
#include "blake3/blake3.h"
/* Provide nonce_cache symbol needed by crypto.c */
struct cache *nonce_cache = NULL;
/* 32-byte and 16-byte PSKs in base64 */
#define PSK32 "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA="
#define PSK16 "AAAAAAAAAAAAAAAAAAAAAA=="
static const char *methods_2022[] = {
"2022-blake3-aes-128-gcm",
"2022-blake3-aes-256-gcm",
"2022-blake3-chacha20-poly1305",
};
static const char *
psk_for(const char *method)
{
return strcmp(method, "2022-blake3-aes-128-gcm") == 0 ? PSK16 : PSK32;
}
static crypto_t *
make_crypto(const char *method)
{
crypto_t *c = crypto_init(psk_for(method), NULL, method);
assert(c != NULL);
return c;
}
static void
free_crypto(crypto_t *c)
{
/*
* crypto_init() sets up the nonce bloom filter each time it is called,
* so it has to be torn down with every crypto_t or each test leaks a
* fresh pair of filters.
*/
ppbloom_free();
ss_free(c->cipher);
ss_free(c);
}
/*
* BLAKE3 derive_key must match the reference implementation, otherwise
* nothing interoperates. Vector generated from the BLAKE3 reference for
* context "shadowsocks 2022 session subkey" over 32 zero bytes of material.
*/
static void
test_blake3_derive_key_known_vector(void)
{
blake3_hasher h;
uint8_t material[32];
uint8_t out[32];
memset(material, 0, sizeof(material));
blake3_hasher_init_derive_key(&h, SS2022_SUBKEY_CTX);
blake3_hasher_update(&h, material, sizeof(material));
blake3_hasher_finalize(&h, out, sizeof(out));
/* Deterministic: the same input must always give the same subkey. */
uint8_t out2[32];
blake3_hasher_init_derive_key(&h, SS2022_SUBKEY_CTX);
blake3_hasher_update(&h, material, sizeof(material));
blake3_hasher_finalize(&h, out2, sizeof(out2));
assert(memcmp(out, out2, 32) == 0);
/* A different context string must give a different subkey. */
uint8_t out3[32];
blake3_hasher_init_derive_key(&h, "some other context");
blake3_hasher_update(&h, material, sizeof(material));
blake3_hasher_finalize(&h, out3, sizeof(out3));
assert(memcmp(out, out3, 32) != 0);
(void)out3;
}
/* BLAKE3 hash of the empty input, from the reference test vectors. */
static void
test_blake3_empty_hash_vector(void)
{
blake3_hasher h;
uint8_t out[32];
static const uint8_t expected[32] = {
0xaf, 0x13, 0x49, 0xb9, 0xf5, 0xf9, 0xa1, 0xa6,
0xa0, 0x40, 0x4d, 0xea, 0x36, 0xdc, 0xc9, 0x49,
0x9b, 0xcb, 0x25, 0xc9, 0xad, 0xc1, 0x12, 0xb7,
0xcc, 0x9a, 0x93, 0xca, 0xe4, 0x1f, 0x32, 0x62
};
blake3_hasher_init(&h);
blake3_hasher_update(&h, "", 0);
blake3_hasher_finalize(&h, out, sizeof(out));
assert(memcmp(out, expected, 32) == 0);
(void)expected;
}
/*
* A client request stream must round-trip through a server decrypt context:
* salt, fixed header, variable header with the SOCKS address, then payload.
*/
static void
test_tcp_request_roundtrip(const char *method)
{
/* SOCKS address: IPv4 1.2.3.4:80 */
static const uint8_t addr[] = { 0x01, 1, 2, 3, 4, 0x00, 0x50 };
static const char *payload = "GET / HTTP/1.0\r\n\r\n";
crypto_t *c = make_crypto(method);
cipher_ctx_t enc, dec;
aead_set_role(CRYPTO_ROLE_CLIENT);
c->ctx_init(c->cipher, &enc, 1);
aead_set_role(CRYPTO_ROLE_SERVER);
c->ctx_init(c->cipher, &dec, 0);
cipher_ctx_pair(&enc, &dec);
/* the contexts keep the role captured at init time */
enc.role = CRYPTO_ROLE_CLIENT;
dec.role = CRYPTO_ROLE_SERVER;
/* First write: the address header. */
buffer_t buf;
memset(&buf, 0, sizeof(buf));
balloc(&buf, 4096);
memcpy(buf.data, addr, sizeof(addr));
buf.len = sizeof(addr);
assert(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK);
/* Second write: stream payload. */
buffer_t buf2;
memset(&buf2, 0, sizeof(buf2));
balloc(&buf2, 4096);
size_t plen = strlen(payload);
memcpy(buf2.data, payload, plen);
buf2.len = plen;
assert(c->encrypt(&buf2, &enc, 4096) == CRYPTO_OK);
/* Feed both writes to the server in one go, as they arrive on the wire. */
buffer_t wire;
memset(&wire, 0, sizeof(wire));
balloc(&wire, 8192);
memcpy(wire.data, buf.data, buf.len);
memcpy(wire.data + buf.len, buf2.data, buf2.len);
wire.len = buf.len + buf2.len;
assert(c->decrypt(&wire, &dec, 8192) == CRYPTO_OK);
/* The server sees the SOCKS address followed by the payload. */
assert(wire.len == sizeof(addr) + plen);
assert(memcmp(wire.data, addr, sizeof(addr)) == 0);
assert(memcmp(wire.data + sizeof(addr), payload, plen) == 0);
bfree(&buf);
bfree(&buf2);
bfree(&wire);
c->ctx_release(&enc);
c->ctx_release(&dec);
free_crypto(c);
}
/*
* A stream split across arbitrary boundaries must still decrypt: the
* decryptor buffers partial chunks and returns CRYPTO_NEED_MORE.
*/
static void
test_tcp_partial_delivery(const char *method)
{
static const uint8_t addr[] = { 0x01, 10, 0, 0, 1, 0x1f, 0x90 };
crypto_t *c = make_crypto(method);
cipher_ctx_t enc, dec;
aead_set_role(CRYPTO_ROLE_CLIENT);
c->ctx_init(c->cipher, &enc, 1);
c->ctx_init(c->cipher, &dec, 0);
cipher_ctx_pair(&enc, &dec);
enc.role = CRYPTO_ROLE_CLIENT;
dec.role = CRYPTO_ROLE_SERVER;
buffer_t buf;
memset(&buf, 0, sizeof(buf));
balloc(&buf, 4096);
memcpy(buf.data, addr, sizeof(addr));
buf.len = sizeof(addr);
assert(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK);
/* Deliver the ciphertext one byte at a time. */
size_t total = buf.len;
int got_ok = 0;
for (size_t i = 0; i < total; i++) {
buffer_t piece;
memset(&piece, 0, sizeof(piece));
balloc(&piece, 4096);
piece.data[0] = buf.data[i];
piece.len = 1;
int rc = c->decrypt(&piece, &dec, 4096);
assert(rc == CRYPTO_OK || rc == CRYPTO_NEED_MORE);
if (rc == CRYPTO_OK) {
assert(piece.len == sizeof(addr));
assert(memcmp(piece.data, addr, sizeof(addr)) == 0);
got_ok = 1;
}
bfree(&piece);
}
assert(got_ok);
(void)got_ok;
bfree(&buf);
c->ctx_release(&enc);
c->ctx_release(&dec);
free_crypto(c);
}
/* A tampered ciphertext must fail authentication rather than pass through. */
static void
test_tcp_tamper_detection(const char *method)
{
static const uint8_t addr[] = { 0x01, 127, 0, 0, 1, 0x00, 0x50 };
crypto_t *c = make_crypto(method);
cipher_ctx_t enc, dec;
aead_set_role(CRYPTO_ROLE_CLIENT);
c->ctx_init(c->cipher, &enc, 1);
c->ctx_init(c->cipher, &dec, 0);
cipher_ctx_pair(&enc, &dec);
enc.role = CRYPTO_ROLE_CLIENT;
dec.role = CRYPTO_ROLE_SERVER;
buffer_t buf;
memset(&buf, 0, sizeof(buf));
balloc(&buf, 4096);
memcpy(buf.data, addr, sizeof(addr));
buf.len = sizeof(addr);
assert(c->encrypt(&buf, &enc, 4096) == CRYPTO_OK);
/* Flip a bit inside the first header chunk (past the salt). */
buf.data[c->cipher->key_len + 2] ^= 0x01;
assert(c->decrypt(&buf, &dec, 4096) == CRYPTO_ERROR);
bfree(&buf);
c->ctx_release(&enc);
c->ctx_release(&dec);
free_crypto(c);
}
/* UDP packets must round-trip through the session interface. */
static void
test_udp_roundtrip(const char *method)
{
static const uint8_t pkt[] = {
0x01, 8, 8, 8, 8, 0x00, 0x35, /* 8.8.8.8:53 */
0xde, 0xad, 0xbe, 0xef /* payload */
};
crypto_t *c = make_crypto(method);
assert(c->encrypt_udp != NULL);
assert(c->decrypt_udp != NULL);
void *client_session = NULL;
void *server_session = NULL;
buffer_t buf;
memset(&buf, 0, sizeof(buf));
balloc(&buf, 2048);
memcpy(buf.data, pkt, sizeof(pkt));
buf.len = sizeof(pkt);
aead_set_role(CRYPTO_ROLE_CLIENT);
assert(c->encrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK);
assert(buf.len > sizeof(pkt));
aead_set_role(CRYPTO_ROLE_SERVER);
assert(c->decrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK);
assert(buf.len == sizeof(pkt));
assert(memcmp(buf.data, pkt, sizeof(pkt)) == 0);
bfree(&buf);
c->udp_session_release(client_session);
c->udp_session_release(server_session);
aead_set_role(CRYPTO_ROLE_CLIENT);
free_crypto(c);
}
/* A replayed UDP packet must be rejected by the sliding window filter. */
static void
test_udp_replay_rejected(const char *method)
{
static const uint8_t pkt[] = { 0x01, 8, 8, 8, 8, 0x00, 0x35, 0x42 };
crypto_t *c = make_crypto(method);
void *client_session = NULL;
void *server_session = NULL;
buffer_t buf;
memset(&buf, 0, sizeof(buf));
balloc(&buf, 2048);
memcpy(buf.data, pkt, sizeof(pkt));
buf.len = sizeof(pkt);
aead_set_role(CRYPTO_ROLE_CLIENT);
assert(c->encrypt_udp(&buf, c->cipher, 2048, &client_session) == CRYPTO_OK);
/* Keep a copy of the wire packet so we can replay it verbatim. */
buffer_t copy;
memset(&copy, 0, sizeof(copy));
balloc(&copy, 2048);
memcpy(copy.data, buf.data, buf.len);
copy.len = buf.len;
aead_set_role(CRYPTO_ROLE_SERVER);
assert(c->decrypt_udp(&buf, c->cipher, 2048, &server_session) == CRYPTO_OK);
/* the very same packet again must be refused */
assert(c->decrypt_udp(&copy, c->cipher, 2048, &server_session) == CRYPTO_ERROR);
bfree(&buf);
bfree(&copy);
c->udp_session_release(client_session);
c->udp_session_release(server_session);
aead_set_role(CRYPTO_ROLE_CLIENT);
free_crypto(c);
}
/* The 2022 ciphers must reject a PSK of the wrong size rather than pad it. */
static void
test_psk_parsing(void)
{
uint8_t key[32];
/* Correct size decodes. */
assert(crypto_parse_psk(PSK32, key, 32) == 32);
for (int i = 0; i < 32; i++)
assert(key[i] == 0);
/*
* The standard alphabet ('+' and '/') and the URL-safe one ('-' and '_')
* must both decode, and to the same bytes: key material is quoted from
* tools using either convention.
*/
uint8_t k1[32], k2[32];
assert(crypto_parse_psk("//////////////////////////////////////////8=", k1, 32) == 32);
assert(crypto_parse_psk("__________________________________________8=", k2, 32) == 32);
assert(memcmp(k1, k2, 32) == 0);
for (int i = 0; i < 32; i++)
assert(k1[i] == 0xff);
/* A key with distinct bytes must decode to exactly those bytes. */
uint8_t k3[32];
assert(crypto_parse_psk("AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=",
k3, 32) == 32);
for (int i = 0; i < 32; i++)
assert(k3[i] == i);
}
int
main(void)
{
if (sodium_init() < 0)
return 1;
test_blake3_empty_hash_vector();
test_blake3_derive_key_known_vector();
test_psk_parsing();
for (size_t i = 0; i < sizeof(methods_2022) / sizeof(methods_2022[0]); i++) {
const char *m = methods_2022[i];
test_tcp_request_roundtrip(m);
test_tcp_partial_delivery(m);
test_tcp_tamper_detection(m);
test_udp_roundtrip(m);
test_udp_replay_rejected(m);
}
return 0;
}
+121
View File
@@ -0,0 +1,121 @@
#!/bin/bash
#
# Interoperability tests against shadowsocks-rust.
#
# The 2022 (SIP022) ciphers are only useful if they interoperate with other
# implementations, and a spec detail can be got wrong in a way that still
# round-trips against ourselves. This drives our binaries against
# shadowsocks-rust's in both directions.
#
# Skips (exit 0) when shadowsocks-rust is not installed, so it is safe to
# run everywhere. Install it with `brew install shadowsocks-rust` or
# `cargo install shadowsocks-rust`.
set -u
BIN_DIR="${SS_BIN_DIR:-build/shared/bin}"
ORIGIN_PORT=${ORIGIN_PORT:-18999}
SERVER_PORT=${SERVER_PORT:-18388}
LOCAL_PORT=${LOCAL_PORT:-18081}
result=0
SPID=""
LPID=""
HPID=""
cleanup() {
[ -n "$SPID" ] && kill -9 "$SPID" 2>/dev/null
[ -n "$LPID" ] && kill -9 "$LPID" 2>/dev/null
[ -n "$HPID" ] && kill -9 "$HPID" 2>/dev/null
wait 2>/dev/null
return 0
}
trap cleanup EXIT
for tool in sslocal ssserver; do
if ! command -v "$tool" > /dev/null 2>&1; then
echo "SKIP: shadowsocks-rust ($tool) not installed"
exit 0
fi
done
for tool in curl python3 openssl; do
if ! command -v "$tool" > /dev/null 2>&1; then
echo "SKIP: $tool not available"
exit 0
fi
done
if [ ! -x "$BIN_DIR/ss-local" ] || [ ! -x "$BIN_DIR/ss-server" ]; then
echo "SKIP: shadowsocks-libev binaries not found in $BIN_DIR"
echo " (set SS_BIN_DIR to override)"
exit 0
fi
# A local origin server keeps the test off the network.
WWW_DIR="$(mktemp -d)"
echo "interop-ok" > "$WWW_DIR/index.html"
(cd "$WWW_DIR" && python3 -m http.server "$ORIGIN_PORT" --bind 127.0.0.1) \
> /dev/null 2>&1 &
HPID=$!
sleep 1
# Fetch through the SOCKS5 proxy and check we got the origin's content back.
fetch_through_proxy() {
curl -s --max-time 15 --socks5-hostname "127.0.0.1:$LOCAL_PORT" \
"http://127.0.0.1:$ORIGIN_PORT/" 2>/dev/null | grep -q "interop-ok"
}
run_case() {
local desc="$1" server_cmd="$2" client_cmd="$3"
printf '\e[0;36mrunning: %s\e[0m\n' "$desc"
$server_cmd > /dev/null 2>&1 &
SPID=$!
sleep 1
$client_cmd > /dev/null 2>&1 &
LPID=$!
sleep 2
if fetch_through_proxy; then
printf '\e[0;32mOK: %s\e[0m\n' "$desc"
else
printf '\e[0;31mFAILED: %s\e[0m\n' "$desc"
result=1
fi
kill -9 "$SPID" "$LPID" 2>/dev/null
wait "$SPID" "$LPID" 2>/dev/null
SPID=""
LPID=""
# let the listening sockets go away before the next case binds them
sleep 2
}
test_method() {
local method="$1" key_size="$2"
local psk
psk="$(openssl rand -base64 "$key_size")"
run_case "$method: libev client -> rust server" \
"ssserver -s 127.0.0.1:$SERVER_PORT -k $psk -m $method" \
"$BIN_DIR/ss-local -s 127.0.0.1 -p $SERVER_PORT -l $LOCAL_PORT -k $psk -m $method"
run_case "$method: rust client -> libev server" \
"$BIN_DIR/ss-server -s 127.0.0.1 -p $SERVER_PORT -k $psk -m $method" \
"sslocal -b 127.0.0.1:$LOCAL_PORT -s 127.0.0.1:$SERVER_PORT -k $psk -m $method"
}
# SIP022. The key sizes are fixed by the spec.
test_method 2022-blake3-aes-128-gcm 16
test_method 2022-blake3-aes-256-gcm 32
test_method 2022-blake3-chacha20-poly1305 32
# A legacy AEAD cipher, to catch a regression that only affects the old path.
test_method aes-256-gcm 32
rm -rf "$WWW_DIR"
exit $result
+231
View File
@@ -0,0 +1,231 @@
/*
* Unit tests for ss:// URI parsing (SIP002 and the legacy form).
*/
#ifdef HAVE_CONFIG_H
#include "config.h"
#endif
#include <assert.h>
#include <string.h>
#include <stdlib.h>
int verbose = 0;
#include "ssurl.h"
#define STREQ(a, b) ((a) != NULL && strcmp((a), (b)) == 0)
/* ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888 */
static void
test_sip002_basic(void)
{
ss_url_t u;
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888",
&u) == 0);
assert(STREQ(u.method, "aes-256-gcm"));
assert(STREQ(u.password, "password"));
assert(STREQ(u.host, "192.168.100.1"));
assert(STREQ(u.port, "8888"));
assert(u.plugin == NULL);
ss_url_free(&u);
}
/* A tag after '#' is descriptive only and must not leak into the fields. */
static void
test_sip002_tag(void)
{
ss_url_t u;
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888"
"#Example%20Server", &u) == 0);
assert(STREQ(u.host, "192.168.100.1"));
assert(STREQ(u.tag, "Example Server"));
ss_url_free(&u);
}
/* SIP003 plugin, with its options after the ';'. */
static void
test_sip002_plugin(void)
{
ss_url_t u;
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@192.168.100.1:8888"
"/?plugin=obfs-local%3Bobfs%3Dhttp#tag", &u) == 0);
assert(STREQ(u.host, "192.168.100.1"));
assert(STREQ(u.port, "8888"));
assert(STREQ(u.plugin, "obfs-local"));
assert(STREQ(u.plugin_opts, "obfs=http"));
assert(STREQ(u.tag, "tag"));
ss_url_free(&u);
}
/* The trailing slash before a query is allowed, and so is no query at all. */
static void
test_sip002_trailing_slash(void)
{
ss_url_t u;
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@example.com:8388/",
&u) == 0);
assert(STREQ(u.host, "example.com"));
assert(STREQ(u.port, "8388"));
ss_url_free(&u);
}
/* A bracketed IPv6 literal must keep its address and port apart. */
static void
test_sip002_ipv6(void)
{
ss_url_t u;
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206cGFzc3dvcmQ@[2001:db8::1]:8388",
&u) == 0);
assert(STREQ(u.host, "2001:db8::1"));
assert(STREQ(u.port, "8388"));
ss_url_free(&u);
}
/* Some producers write the userinfo in the clear rather than base64. */
static void
test_sip002_plain_userinfo(void)
{
ss_url_t u;
assert(ss_url_parse("ss://aes-256-gcm:secret@example.com:8388", &u) == 0);
assert(STREQ(u.method, "aes-256-gcm"));
assert(STREQ(u.password, "secret"));
assert(STREQ(u.host, "example.com"));
ss_url_free(&u);
}
/*
* A 2022 pre-shared key is base64 and can contain '+', '/' and '='. Encoded
* into the userinfo it must survive the round trip byte for byte.
*/
static void
test_sip002_2022_psk(void)
{
ss_url_t u;
/* base64("2022-blake3-aes-256-gcm:ZG9uJ3QgdXNlIHRoaXMga2V5Pz8/Pz8/Pz8=") */
const char *uri =
"ss://MjAyMi1ibGFrZTMtYWVzLTI1Ni1nY206Wkc5dUozUWdkWE5sSUhSb2FYTWdhMlY1"
"Pz8_Pz8_Pz89@example.com:8388";
assert(ss_url_parse(uri, &u) == 0);
assert(STREQ(u.method, "2022-blake3-aes-256-gcm"));
assert(STREQ(u.host, "example.com"));
assert(STREQ(u.port, "8388"));
ss_url_free(&u);
}
/* Legacy form: the whole body is one base64 blob. */
static void
test_legacy(void)
{
ss_url_t u;
/* base64("aes-256-cfb:password@192.168.100.1:8888") */
assert(ss_url_parse("ss://YWVzLTI1Ni1jZmI6cGFzc3dvcmRAMTkyLjE2OC4xMDAuMTo4ODg4",
&u) == 0);
assert(STREQ(u.method, "aes-256-cfb"));
assert(STREQ(u.password, "password"));
assert(STREQ(u.host, "192.168.100.1"));
assert(STREQ(u.port, "8888"));
ss_url_free(&u);
}
/* Malformed input must be rejected, not half-parsed. */
static void
test_rejects_malformed(void)
{
ss_url_t u;
static const char *bad[] = {
NULL,
"",
"http://example.com",
"ss://",
"ss://@example.com:8388", /* empty userinfo */
"ss://YWVzOnB3@example.com", /* no port */
"ss://YWVzOnB3@:8388", /* no host */
"ss://YWVzOnB3@example.com:", /* empty port */
"ss://bm9jb2xvbg@example.com:8388", /* userinfo lacks ':' */
"ss://YWVzOnB3@[2001:db8::1", /* unterminated bracket */
};
for (size_t i = 0; i < sizeof(bad) / sizeof(bad[0]); i++) {
assert(ss_url_parse(bad[i], &u) == -1);
/* a rejected parse must leave nothing behind to free */
assert(u.method == NULL && u.host == NULL && u.port == NULL);
}
}
/*
* URIs emitted by shadowsocks-rust's `ssurl --encode`, copied verbatim.
* These pin the shapes another implementation actually produces, which is
* what the parser has to accept in the field.
*/
static void
test_rust_ssurl_output(void)
{
ss_url_t u;
/* A 2022 cipher: plain userinfo, '=' of the key percent-encoded. */
assert(ss_url_parse("ss://2022-blake3-aes-256-gcm:"
"ZG9uJ3QgdXNlIHRoaXMga2V5ISEhISEhISEhISEhISE%3D"
"@192.168.100.1:8888", &u) == 0);
assert(STREQ(u.method, "2022-blake3-aes-256-gcm"));
assert(STREQ(u.password, "ZG9uJ3QgdXNlIHRoaXMga2V5ISEhISEhISEhISEhISE="));
assert(STREQ(u.host, "192.168.100.1"));
assert(STREQ(u.port, "8888"));
ss_url_free(&u);
/* A legacy cipher: unpadded base64 userinfo, plugin in the query. */
assert(ss_url_parse("ss://YWVzLTI1Ni1nY206dGVzdC1wYXNzd29yZA"
"@192.168.100.1:8888/?plugin=obfs%2Dlocal%3Bobfs%3Dhttp",
&u) == 0);
assert(STREQ(u.method, "aes-256-gcm"));
assert(STREQ(u.password, "test-password"));
assert(STREQ(u.plugin, "obfs-local"));
assert(STREQ(u.plugin_opts, "obfs=http"));
ss_url_free(&u);
/* An IPv6 server. */
assert(ss_url_parse("ss://Y2hhY2hhMjAtaWV0Zi1wb2x5MTMwNTpwdw"
"@[2001:db8::1]:8388", &u) == 0);
assert(STREQ(u.method, "chacha20-ietf-poly1305"));
assert(STREQ(u.password, "pw"));
assert(STREQ(u.host, "2001:db8::1"));
assert(STREQ(u.port, "8388"));
ss_url_free(&u);
}
/* Freeing a zeroed struct, or NULL, must be safe. */
static void
test_free_is_safe(void)
{
ss_url_t u = { 0 };
ss_url_free(&u);
ss_url_free(NULL);
}
int
main(void)
{
test_sip002_basic();
test_sip002_tag();
test_sip002_plugin();
test_sip002_trailing_slash();
test_sip002_ipv6();
test_sip002_plain_userinfo();
test_sip002_2022_psk();
test_legacy();
test_rust_ssurl_output();
test_rejects_malformed();
test_free_is_safe();
return 0;
}