boo-rs 0.1.3

Encrypt primitives types at compile time
Documentation
//! # Boo
//!
//! Boo encrypts literal data in the final binary, at compile time, preventing static analysis tools from
//! reading values.
//!
//! # Usage
//!
//! Add the dependency:
//!
//! ```toml
//! [dependencies]
//! boo-rs = "0.1"
//! ```
//!
//! Set an optional encryption key (or fallback to a 64-byte randomly generated one):
//!
//! ```bash
//! export BOO_KEY="secret-key"
//! ```
//!
//! # Example
//!
//! ```rust
//! extern crate alloc;
//! #[macro_use]
//! extern crate boo_rs;
//!
//! boo_init!();
//!
//! #[allow(unused_variables)]
//! fn main() {
//!     let n = boo!(3);
//!     let text = boo!("hello");
//!     let bytes = boo!(b"\x01\x02\x03");
//!     let pair = boo!(("host", 443));
//!     let nested = boo!([[1, 2], [3, 4]]);
//! }
//! ```
//!
//! `boo_init!()` must be called once before using the `boo!()` macro.
//! After that, the macro can be used anywhere to encrypt almost all Rust literal values.
//!
//! # Performance
//!
//! Decryption happens on the stack. The cost is O(n), where n is the length of the data in bytes.
//!
//! - All decrypted types except `String` and `CStr` are stored on the stack without performance overhead.
//! - `&str` and `&CStr` decryption are stored into their heap-allocated variants.
//! - Special case: binary strings are decrypted into owned `[u8]` arrays.

extern crate alloc;
extern crate core;
extern crate proc_macro;
extern crate proc_macro2;
extern crate quote;
extern crate rand;
extern crate syn;

use std::fs;
use std::path::{Path, PathBuf};
use std::sync::LazyLock;

use proc_macro2::Literal;
use quote::quote;
use syn::{Expr, ExprLit, Lit};

use crate::branch::Branch;
use crate::literal_bytes::LiteralBytes;

mod branch;
mod literal_bytes;
#[cfg(test)]
mod mangle;
#[cfg(test)]
mod test;
mod utils;

const INCLUDE_ERROR: &str = r#"expected one file path (ex. "data.txt")"#;

/// Cryptographic key
static KEY: LazyLock<Box<[u8]>> = LazyLock::new(|| match option_env!("BOO_KEY") {
    Some(key) => key.as_bytes().into(),
    None => {
        let mut key = [0; 64];
        rand::fill(&mut key);

        key.into()
    }
});

/// Initialize the boo library allowing use of the boo macros.
///
/// Optionally set a custom key using the `BOO_KEY` environment variable.
/// Fallbacks to a random 64-bytes cryptographic key.
///
/// # Important
///
/// `boo_init!()` must be called once before using the `boo!()` macros.
///
/// # Example
///
/// ```
/// # extern crate alloc;
/// # #[macro_use] extern crate boo_rs;
/// boo_init!();
/// ```
#[proc_macro]
pub fn boo_init(_tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let key = Literal::byte_string(&KEY);
    let utils = syn::parse_str::<syn::File>(include_str!("utils.rs")).unwrap();

    let result = quote! {
        static BOO_KEY: &[u8] = #key;

        pub mod __boo {
            #utils
        }
    };

    result.into()
}

/// Encrypts a literal
///
/// The embedded ciphertext carries a sibling checksum, verified at runtime before decryption:
/// this catches a direct edit to the ciphertext bytes in the compiled binary (the checksum
/// constant is a separate embedded value the edit doesn't touch), not an edit to the decrypted
/// plaintext or to code that reads it, and not a patch that also recomputes this checksum.
///
/// # Performance
///
/// Decryption happens on the stack. The cost is O(n), where n is the length of the data in bytes.
///
/// All decrypted types except `String` and `CStr` are stored on the stack without performance overhead.
///
/// # Panics
///
/// At macro-expansion time, panics if the input isn't a supported literal. At runtime, panics
/// if the embedded ciphertext no longer matches its embedded checksum.
///
/// # Returns
///
/// The same value that is passed in
///
/// # Example
///
/// ```
/// # extern crate alloc;
/// # #[macro_use] extern crate boo_rs;
/// boo_init!();
/// fn main() {
///     assert_eq!(boo!("boo"), "boo");
/// }
/// ```
#[proc_macro]
pub fn boo(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let site_salt = call_site_salt();
    let literal = match LiteralBytes::parse(tokens.into()) {
        Ok(literal) => literal,
        Err(err) => panic!("{err}"),
    };

    literal.encrypt(site_salt).into()
}

/// Branches on a boolean condition through an opaque-predicate dispatch instead of a plain
/// `if`/`else`.
///
/// Raises the cost of spotting the branch in source review or a static disassembler's xrefs: the
/// dispatch is an XOR-masked comparison salted per call site, not a direct test of the condition,
/// hidden from the optimizer via [`core::hint::black_box`] to keep a release build from folding
/// it back to a plain branch. This still offers no resistance to a live debugger stepping through
/// the code - same limits as the crate's literal encryption.
///
/// # Panics
///
/// Panics if the input isn't `cond { .. } else { .. }`. `else if` chains aren't supported.
///
/// # Example
///
/// ```
/// # extern crate alloc;
/// # #[macro_use] extern crate boo_rs;
/// # boo_init!();
/// let licensed = true;
/// assert_eq!(boo_branch!(licensed { "full" } else { "trial" }), "full");
/// ```
#[proc_macro]
pub fn boo_branch(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let site_salt = call_site_salt();
    let branch = match syn::parse2::<Branch>(tokens.into()) {
        Ok(branch) => branch,
        Err(err) => panic!("{err}"),
    };

    branch.obscure(site_salt).into()
}

/// Publishes `Mangled`, mangling a fixed-size value against this process's cookie and a fresh
/// per-value nonce for storage.
///
/// Mirrors glibc's `PTR_MANGLE`/Windows' `EncodePointer`: masks a fixed-size value against a
/// cookie generated once at process start, folded per value with a nonce - no two mangled values
/// share a keystream. A value a live memory-editing tool saves in one run decodes to garbage after
/// a restart - see `mangle.rs` for the mechanism.
///
/// `Mangled` is an ordinary type from here on: `Mangled::new(value)` to store, and
/// `mangled.reveal_with(|plain| ..)` to read it back - the plaintext never outlives that closure.
/// No further macro involved on either path; this one only exists to paste a private copy of the
/// mangling code into the calling crate, keeping its symbol private to each dependent crate.
///
/// # Important
///
/// `boo_mangle_init!()` must be called once, at the crate root, before using `Mangled`.
///
/// # Example
///
/// ```
/// # extern crate alloc;
/// # #[macro_use] extern crate boo_rs;
/// # boo_init!();
/// boo_mangle_init!();
///
/// fn main() {
///     let original = [1u8, 2, 3, 4];
///     let mangled = Mangled::new(original);
///     mangled.reveal_with(|plain| assert_eq!(*plain, original));
/// }
/// ```
#[proc_macro]
pub fn boo_mangle_init(_tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let mangle = syn::parse_str::<syn::File>(include_str!("mangle.rs")).unwrap();

    let result = quote! {
        mod __boo_mangle {
            #mangle
        }

        pub use __boo_mangle::Mangled;
    };

    result.into()
}

/// Per-invocation salt derived from this macro call's line/column and the process-wide [`KEY`].
///
/// Never uses `file!()`: a source path embedded in the salt would leak into the compiled binary.
fn call_site_salt() -> u64 {
    use std::hash::Hasher;

    let start = proc_macro2::Span::call_site().start();
    let mut hasher = std::collections::hash_map::DefaultHasher::new();
    hasher.write_usize(start.line);
    hasher.write_usize(start.column);
    hasher.write(&KEY);
    hasher.finish()
}

/// Encrypts a raw file as bytes
///
/// # Performance
///
/// Decryption happens on the stack. The cost is O(n), where n is the length of the data in bytes.
///
/// # Returns
///
/// File content as bytes
///
/// # Example
///
/// ```ignore
/// # extern crate alloc;
/// # #[macro_use] extern crate boo_rs;
/// # boo_init!();
/// let my_file = boo_include_bytes!("my-file.txt");
/// ```
#[proc_macro]
pub fn boo_include_bytes(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let Some(file_path) = read_literal_str(tokens) else {
        panic!("{INCLUDE_ERROR}");
    };
    let file_path = relative_path(&file_path);

    let data = match fs::read(file_path) {
        Ok(data) => data,
        Err(err) => panic!("Failed to read the file: {err}"),
    };

    LiteralBytes::ByteStr(data).encrypt(call_site_salt()).into()
}

/// Encrypts a UTF-8 file as a string
///
/// # Performance
///
/// Decryption happens on the stack. The cost is O(n), where n is the length of the data in bytes.
///
/// # Returns
///
/// File content as string
///
/// # Example
///
/// ```ignore
/// # extern crate alloc;
/// # #[macro_use] extern crate boo_rs;
/// # boo_init!();
/// let my_file = boo_include_str!("my-file.txt");
/// ```
#[proc_macro]
pub fn boo_include_str(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let Some(file_path) = read_literal_str(tokens) else {
        panic!("{INCLUDE_ERROR}");
    };
    let file_path = relative_path(&file_path);

    let data = match fs::read_to_string(file_path) {
        Ok(data) => data,
        Err(err) => panic!("Failed to read the file: {err}"),
    };

    LiteralBytes::Str(data.into_bytes())
        .encrypt(call_site_salt())
        .into()
}

/// Encrypts raw file from the path specified by an environment variable
///
/// # Performance
///
/// Decryption happens on the stack. The cost is O(n), where n is the length of the data in bytes.
///
/// # Returns
///
/// File content as bytes
///
/// # Example
///
/// ```ignore
/// # extern crate alloc;
/// # #[macro_use] extern crate boo_rs;
/// # boo_init!();
/// // Requires ENV_FILE to be set to an existing path
/// let my_file = boo_include_env!("ENV_FILE");
/// ```
#[proc_macro]
pub fn boo_include_env(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
    let Some(env_var) = read_literal_str(tokens) else {
        panic!("{INCLUDE_ERROR}");
    };
    let file_path = std::env::var(env_var).expect("Environment variable must exists.");

    let data = match fs::read(file_path) {
        Ok(data) => data,
        Err(err) => panic!("Failed to read the file: {err}"),
    };

    LiteralBytes::ByteStr(data).encrypt(call_site_salt()).into()
}

/// Reads a single string literal from a token stream
///
/// # Arguments
///
/// * `tokens` - Token stream containing a single string literal
fn read_literal_str(tokens: proc_macro::TokenStream) -> Option<String> {
    if let Ok(Expr::Lit(ExprLit {
        lit: Lit::Str(str), ..
    })) = syn::parse2::<Expr>(tokens.into())
    {
        return Some(str.value());
    }

    None
}

/// Makes a path relative to the calling source code file
fn relative_path(path: &str) -> PathBuf {
    let current_dir = Path::new(file!())
        .parent()
        .unwrap_or_else(|| Path::new("."));

    current_dir.join(path)
}