Compare commits

...

4 Commits

11 changed files with 334 additions and 11 deletions

View File

@ -13,6 +13,11 @@
//! * `WOLFSSL_INSTALL_DIR` If set, skip the CMake build and link against
//! a pre-installed wolfSSL at this path. The path must contain
//! `include/wolfssl/` and `lib/libwolfssl.a`.
//! * `ANDROID_NDK_ROOT` (or `ANDROID_NDK_HOME`) Required when targeting
//! Android. Set to the NDK root directory so that CMake can locate the
//! Android toolchain. Cargokit/Gradle sets this automatically; when
//! invoking `cargo` directly make sure the variable is exported in your
//! shell session.
use std::{
env,
@ -99,6 +104,31 @@ fn build_wolfssl_cmake(source_dir: &Path, _out_dir: &Path) -> (PathBuf, PathBuf)
let mut cfg = cmake::Config::new(source_dir);
// ── Android NDK ───────────────────────────────────────────────────────────
// When cross-compiling for Android the cmake crate already sets
// CMAKE_SYSTEM_NAME=Android and the clang compiler paths. The only
// missing piece that Android-Determine.cmake checks first is
// CMAKE_ANDROID_NDK — once it is set all other NDK detection succeeds.
//
// We read it from ANDROID_NDK_ROOT (standard SDK Manager layout) or the
// legacy ANDROID_NDK_HOME. Cargokit/Gradle sets ANDROID_NDK_ROOT, and the
// same variable must be visible in the shell when using cargo directly.
let target_os = env::var("CARGO_CFG_TARGET_OS").unwrap_or_default();
if target_os == "android" {
let ndk_root = env::var("ANDROID_NDK_ROOT")
.or_else(|_| env::var("ANDROID_NDK_HOME"))
.unwrap_or_else(|_| {
panic!(
"\n\nAndroid target requires ANDROID_NDK_ROOT (or ANDROID_NDK_HOME) \
to be set.\nSet it to the NDK directory, e.g.:\n \
export ANDROID_NDK_ROOT=$HOME/apps/android-sdk/ndk/28.x.x\n"
)
});
cfg.define("CMAKE_ANDROID_NDK", &ndk_root);
println!("cargo:rerun-if-env-changed=ANDROID_NDK_ROOT");
println!("cargo:rerun-if-env-changed=ANDROID_NDK_HOME");
}
// ── Core algorithm selection ─────────────────────────────────────────────
// wolfSSL cmake uses add_option() with "yes"/"no" string values (not ON/OFF).
// Option names must exactly match the `add_option("NAME" ...)` calls in

View File

@ -343,16 +343,18 @@ fn xchacha20_poly1305_encrypt(
unsafe {
// wc_XChaCha20Poly1305_Encrypt(dst, dst_space, src, src_len,
// ad, ad_len, nonce, nonce_len, key, key_len)
//
// NOTE: wolfSSL's wc_XChaCha20Poly1305_Init unconditionally rejects
// a NULL `ad` pointer (BAD_FUNC_ARG / -173), unlike AES-GCM and
// ChaCha20-Poly1305 which allow NULL when ad_len == 0. Always pass
// aad.as_ptr() — Rust guarantees a non-null dangling pointer for
// empty slices.
let ret = crate::sys::wc_XChaCha20Poly1305_Encrypt(
dst.as_mut_ptr(),
dst.len(),
plaintext.as_ptr(),
plaintext.len(),
if aad.is_empty() {
std::ptr::null()
} else {
aad.as_ptr()
},
aad.as_ptr(),
aad.len(),
nonce.as_ptr(),
nonce.len(),
@ -387,16 +389,14 @@ fn xchacha20_poly1305_decrypt(
unsafe {
// wc_XChaCha20Poly1305_Decrypt(dst, dst_space, src, src_len,
// ad, ad_len, nonce, nonce_len, key, key_len)
//
// NOTE: See encrypt above — wolfSSL rejects NULL ad for XChaCha20.
let ret = crate::sys::wc_XChaCha20Poly1305_Decrypt(
dst.as_mut_ptr(),
dst.len(),
ciphertext_and_tag.as_ptr(),
ciphertext_and_tag.len(),
if aad.is_empty() {
std::ptr::null()
} else {
aad.as_ptr()
},
aad.as_ptr(),
aad.len(),
nonce.as_ptr(),
nonce.len(),
@ -410,3 +410,280 @@ fn xchacha20_poly1305_decrypt(
Ok(dst)
}
// ──────────────────────────────────────────────────────────────────────────────
// Tests
// ──────────────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use ccc_crypto_core::algorithms::AeadAlgorithm;
// ── Helpers ──────────────────────────────────────────────────────────
/// Encrypt then decrypt and assert the plaintext round-trips.
fn roundtrip(algo: AeadAlgorithm, key: &[u8], nonce: &[u8], pt: &[u8], aad: &[u8]) {
let ct = encrypt(algo, key, nonce, pt, aad)
.unwrap_or_else(|e| panic!("{} encrypt failed: {e}", algo.name()));
// ciphertext must be plaintext + 16-byte tag
assert_eq!(
ct.len(),
pt.len() + TAG_LEN,
"{}: unexpected ciphertext length",
algo.name()
);
let recovered = decrypt(algo, key, nonce, &ct, aad)
.unwrap_or_else(|e| panic!("{} decrypt failed: {e}", algo.name()));
assert_eq!(recovered, pt, "{}: plaintext mismatch after roundtrip", algo.name());
}
// ── AES-256-GCM ─────────────────────────────────────────────────────
#[test]
fn aes_gcm_roundtrip_with_aad() {
roundtrip(
AeadAlgorithm::AesGcm256,
&[0x01; 32],
&[0x02; 12],
b"aes-gcm payload with associated data",
b"some associated data",
);
}
#[test]
fn aes_gcm_roundtrip_empty_aad() {
roundtrip(
AeadAlgorithm::AesGcm256,
&[0x11; 32],
&[0x22; 12],
b"aes-gcm payload no aad",
&[],
);
}
#[test]
fn aes_gcm_roundtrip_empty_plaintext() {
roundtrip(
AeadAlgorithm::AesGcm256,
&[0x31; 32],
&[0x32; 12],
&[],
b"aad only, no plaintext",
);
}
#[test]
fn aes_gcm_roundtrip_empty_both() {
roundtrip(
AeadAlgorithm::AesGcm256,
&[0x41; 32],
&[0x42; 12],
&[],
&[],
);
}
#[test]
fn aes_gcm_tampered_tag_fails() {
let key = [0x51u8; 32];
let nonce = [0x52u8; 12];
let pt = b"integrity check";
let mut ct = encrypt(AeadAlgorithm::AesGcm256, &key, &nonce, pt, &[]).unwrap();
// Flip last byte of the tag
let last = ct.len() - 1;
ct[last] ^= 0xFF;
assert!(decrypt(AeadAlgorithm::AesGcm256, &key, &nonce, &ct, &[]).is_err());
}
// ── ChaCha20-Poly1305 ───────────────────────────────────────────────
#[test]
fn chacha20_roundtrip_with_aad() {
roundtrip(
AeadAlgorithm::ChaCha20Poly1305,
&[0x61; 32],
&[0x62; 12],
b"chacha20 payload with associated data",
b"chacha20 aad",
);
}
#[test]
fn chacha20_roundtrip_empty_aad() {
roundtrip(
AeadAlgorithm::ChaCha20Poly1305,
&[0x71; 32],
&[0x72; 12],
b"chacha20 payload no aad",
&[],
);
}
#[test]
fn chacha20_roundtrip_empty_plaintext() {
roundtrip(
AeadAlgorithm::ChaCha20Poly1305,
&[0x81; 32],
&[0x82; 12],
&[],
b"chacha20 aad only",
);
}
#[test]
fn chacha20_roundtrip_empty_both() {
roundtrip(
AeadAlgorithm::ChaCha20Poly1305,
&[0x91; 32],
&[0x92; 12],
&[],
&[],
);
}
#[test]
fn chacha20_tampered_tag_fails() {
let key = [0xA1u8; 32];
let nonce = [0xA2u8; 12];
let pt = b"integrity check chacha";
let mut ct = encrypt(AeadAlgorithm::ChaCha20Poly1305, &key, &nonce, pt, &[]).unwrap();
let last = ct.len() - 1;
ct[last] ^= 0xFF;
assert!(decrypt(AeadAlgorithm::ChaCha20Poly1305, &key, &nonce, &ct, &[]).is_err());
}
// ── XChaCha20-Poly1305 ──────────────────────────────────────────────
#[test]
fn xchacha20_roundtrip_with_aad() {
roundtrip(
AeadAlgorithm::XChaCha20Poly1305,
&[0xB1; 32],
&[0xB2; 24],
b"xchacha20 payload with associated data",
b"xchacha20 aad",
);
}
/// Regression: wolfSSL's `wc_XChaCha20Poly1305_Init` unconditionally
/// rejects a NULL `ad` pointer (BAD_FUNC_ARG / -173), even when
/// `ad_len == 0`. Must pass a non-null dangling pointer for empty AAD.
#[test]
fn xchacha20_roundtrip_empty_aad() {
roundtrip(
AeadAlgorithm::XChaCha20Poly1305,
&[0xC1; 32],
&[0xC2; 24],
b"xchacha20 payload no aad",
&[],
);
}
#[test]
fn xchacha20_roundtrip_empty_plaintext() {
roundtrip(
AeadAlgorithm::XChaCha20Poly1305,
&[0xD1; 32],
&[0xD2; 24],
&[],
b"xchacha20 aad only",
);
}
/// Regression: both AAD and plaintext empty — exercises the NULL-pointer
/// code paths simultaneously.
#[test]
fn xchacha20_roundtrip_empty_both() {
roundtrip(
AeadAlgorithm::XChaCha20Poly1305,
&[0xE1; 32],
&[0xE2; 24],
&[],
&[],
);
}
#[test]
fn xchacha20_tampered_tag_fails() {
let key = [0xF1u8; 32];
let nonce = [0xF2u8; 24];
let pt = b"integrity check xchacha";
let mut ct = encrypt(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, pt, &[]).unwrap();
let last = ct.len() - 1;
ct[last] ^= 0xFF;
assert!(decrypt(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, &ct, &[]).is_err());
}
#[test]
fn xchacha20_wrong_aad_fails() {
let key = [0xF3u8; 32];
let nonce = [0xF4u8; 24];
let pt = b"aad mismatch test";
let ct = encrypt(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, pt, b"correct").unwrap();
assert!(decrypt(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, &ct, b"wrong").is_err());
}
// ── Key / nonce validation ──────────────────────────────────────────
#[test]
fn aes_gcm_wrong_key_len_rejected() {
assert!(encrypt(AeadAlgorithm::AesGcm256, &[0; 16], &[0; 12], b"x", &[]).is_err());
}
#[test]
fn aes_gcm_wrong_nonce_len_rejected() {
assert!(encrypt(AeadAlgorithm::AesGcm256, &[0; 32], &[0; 8], b"x", &[]).is_err());
}
#[test]
fn chacha20_wrong_key_len_rejected() {
assert!(encrypt(AeadAlgorithm::ChaCha20Poly1305, &[0; 16], &[0; 12], b"x", &[]).is_err());
}
#[test]
fn chacha20_wrong_nonce_len_rejected() {
assert!(encrypt(AeadAlgorithm::ChaCha20Poly1305, &[0; 32], &[0; 8], b"x", &[]).is_err());
}
#[test]
fn xchacha20_wrong_key_len_rejected() {
assert!(encrypt(AeadAlgorithm::XChaCha20Poly1305, &[0; 16], &[0; 24], b"x", &[]).is_err());
}
#[test]
fn xchacha20_wrong_nonce_len_rejected() {
assert!(encrypt(AeadAlgorithm::XChaCha20Poly1305, &[0; 32], &[0; 12], b"x", &[]).is_err());
}
// ── Large payload ───────────────────────────────────────────────────
#[test]
fn xchacha20_large_payload_roundtrip() {
let key = [0xAAu8; 32];
let nonce = [0xBBu8; 24];
let pt = vec![0x42u8; 64 * 1024]; // 64 KiB — exercises the 16k chunked loop
roundtrip(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce, &pt, b"large");
}
// ── Cross-algorithm isolation ───────────────────────────────────────
#[test]
fn ciphertext_not_interchangeable_between_algorithms() {
let key = [0xDD; 32];
let nonce_12 = [0xEE; 12];
let nonce_24 = [0xEE; 24];
let pt = b"cross-algo test";
let ct_aes = encrypt(AeadAlgorithm::AesGcm256, &key, &nonce_12, pt, &[]).unwrap();
let ct_chacha = encrypt(AeadAlgorithm::ChaCha20Poly1305, &key, &nonce_12, pt, &[]).unwrap();
let ct_xchacha = encrypt(AeadAlgorithm::XChaCha20Poly1305, &key, &nonce_24, pt, &[]).unwrap();
// Each ciphertext must fail to decrypt under a different algorithm
assert!(decrypt(AeadAlgorithm::ChaCha20Poly1305, &key, &nonce_12, &ct_aes, &[]).is_err());
assert!(decrypt(AeadAlgorithm::AesGcm256, &key, &nonce_12, &ct_chacha, &[]).is_err());
assert!(decrypt(AeadAlgorithm::AesGcm256, &key, &nonce_24, &ct_xchacha, &[]).is_err());
}
}

View File

@ -1,5 +1,5 @@
# https://taskfile.dev
version: "3"
silent: true # This makes all tasks silent by default
vars:
APPLE_TARGETS: "aarch64-apple-ios aarch64-apple-darwin x86_64-apple-darwin"
@ -201,6 +201,22 @@ tasks:
- task: release
- task: build:apple
# ── Publish ─────────────────────────────────────────────────────────────
release:
desc: "Push trunk and sync lum_ccc_fplugin Cargo.lock to the new commit"
summary: |
Runs in order:
1. git push gitea trunk (lum_ccc_rust)
2. dev-sync.sh --update (lum_ccc_fplugin) — updates Cargo.lock + clears build cache
Set FPLUGIN_DIR env var to override the default fplugin path.
vars:
FPLUGIN_DIR:
sh: echo "${FPLUGIN_DIR:-/Volumes/LUM/source/letusmsg_proj/app/lum_ccc_fplugin}"
cmds:
- git push gitea trunk
- "{{.FPLUGIN_DIR}}/scripts/dev-sync.sh --update"
# ── Utility ─────────────────────────────────────────────────────────────
clean: