Skip to main content

boo_rs/
lib.rs

1//! # Boo
2//!
3//! Boo encrypts literal data in the final binary, at compile time, preventing static analysis tools from
4//! reading values.
5//!
6//! # Usage
7//!
8//! Add the dependency:
9//!
10//! ```toml
11//! [dependencies]
12//! boo-rs = "0.1"
13//! ```
14//!
15//! Set an optional encryption key (or fallback to a 64-byte randomly generated one):
16//!
17//! ```bash
18//! export BOO_KEY="secret-key"
19//! ```
20//!
21//! # Example
22//!
23//! ```rust
24//! extern crate alloc;
25//! #[macro_use]
26//! extern crate boo_rs;
27//!
28//! boo_init!();
29//!
30//! #[allow(unused_variables)]
31//! fn main() {
32//!     let n = boo!(3);
33//!     let text = boo!("hello");
34//!     let bytes = boo!(b"\x01\x02\x03");
35//!     let pair = boo!(("host", 443));
36//!     let nested = boo!([[1, 2], [3, 4]]);
37//! }
38//! ```
39//!
40//! `boo_init!()` must be called once before using the `boo!()` macro.
41//! After that, the macro can be used anywhere to encrypt almost all Rust literal values.
42//!
43//! # Performance
44//!
45//! Decryption happens on the stack. The cost is O(n), where n is the length of the data in bytes.
46//!
47//! - All decrypted types except `String` and `CStr` are stored on the stack without performance overhead.
48//! - `&str` and `&CStr` decryption are stored into their heap-allocated variants.
49//! - Special case: binary strings are decrypted into owned `[u8]` arrays.
50
51extern crate alloc;
52extern crate core;
53extern crate proc_macro;
54extern crate proc_macro2;
55extern crate quote;
56extern crate rand;
57extern crate syn;
58
59use std::fs;
60use std::path::{Path, PathBuf};
61use std::sync::LazyLock;
62
63use proc_macro2::Literal;
64use quote::quote;
65use syn::{Expr, ExprLit, Lit};
66
67use crate::literal_bytes::LiteralBytes;
68
69mod literal_bytes;
70#[cfg(test)]
71mod test;
72mod utils;
73
74const INCLUDE_ERROR: &str = r#"expected one file path (ex. "data.txt")"#;
75
76/// Cryptographic key
77static KEY: LazyLock<Box<[u8]>> = LazyLock::new(|| match option_env!("BOO_KEY") {
78    Some(key) => key.as_bytes().into(),
79    None => {
80        let mut key = [0; 64];
81        rand::fill(&mut key);
82
83        key.into()
84    }
85});
86
87/// Initialize the boo library allowing use of the boo macros.
88///
89/// Optionally set a custom key using the `BOO_KEY` environment variable.
90/// Fallbacks to a random 64-bytes cryptographic key.
91///
92/// # Important
93///
94/// `boo_init!()` must be called once before using the `boo!()` macros.
95///
96/// # Example
97///
98/// ```
99/// boo_init!();
100/// ```
101#[proc_macro]
102pub fn boo_init(_tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
103    let key = Literal::byte_string(&KEY);
104    let utils = syn::parse_str::<syn::File>(include_str!("utils.rs")).unwrap();
105
106    let result = quote! {
107        static BOO_KEY: &[u8] = #key;
108
109        pub mod __boo {
110            #utils
111        }
112    };
113
114    result.into()
115}
116
117/// Encrypts a literal
118///
119/// # Performance
120///
121/// Decryption happens on the stack. The cost is O(n), where n is the length of the data in bytes.
122///
123/// All decrypted types except `String` and `CStr` are stored on the stack without performance overhead.
124///
125/// # Returns
126///
127/// The same value that is passed in
128///
129/// # Example
130///
131/// ```
132/// assert_eq!(boo!("boo"), "boo");
133/// ```
134#[proc_macro]
135pub fn boo(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
136    let literal = match LiteralBytes::parse(tokens.into()) {
137        Ok(literal) => literal,
138        Err(err) => panic!("{err}"),
139    };
140
141    literal.encrypt().into()
142}
143
144/// Encrypts a raw file as bytes
145///
146/// # Performance
147///
148/// Decryption happens on the stack. The cost is O(n), where n is the length of the data in bytes.
149///
150/// # Returns
151///
152/// File content as bytes
153///
154/// # Example
155///
156/// ```
157/// let my_file = boo_include_bytes!("my-file.txt");
158/// ```
159#[proc_macro]
160pub fn boo_include_bytes(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
161    let Some(file_path) = read_literal_str(tokens) else {
162        panic!("{INCLUDE_ERROR}");
163    };
164    let file_path = relative_path(&file_path);
165
166    let data = match fs::read(file_path) {
167        Ok(data) => data,
168        Err(err) => panic!("Failed to read the file: {err}"),
169    };
170
171    LiteralBytes::ByteStr(data).encrypt().into()
172}
173
174/// Encrypts a UTF-8 file as a string
175///
176/// # Performance
177///
178/// Decryption happens on the stack. The cost is O(n), where n is the length of the data in bytes.
179///
180/// # Returns
181///
182/// File content as string
183///
184/// # Example
185///
186/// ```
187/// let my_file = boo_include_str!("my-file.txt");
188/// ```
189#[proc_macro]
190pub fn boo_include_str(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
191    let Some(file_path) = read_literal_str(tokens) else {
192        panic!("{INCLUDE_ERROR}");
193    };
194    let file_path = relative_path(&file_path);
195
196    let data = match fs::read_to_string(file_path) {
197        Ok(data) => data,
198        Err(err) => panic!("Failed to read the file: {err}"),
199    };
200
201    LiteralBytes::Str(data.into_bytes()).encrypt().into()
202}
203
204/// Encrypts raw file from the path specified by an environment variable
205///
206/// # Performance
207///
208/// Decryption happens on the stack. The cost is O(n), where n is the length of the data in bytes.
209///
210/// # Returns
211///
212/// File content as bytes
213///
214/// # Example
215///
216/// ```
217/// // Requires ENV_FILE to be set to an existing path
218/// let my_file = boo_include_env!("ENV_FILE");
219/// ```
220#[proc_macro]
221pub fn boo_include_env(tokens: proc_macro::TokenStream) -> proc_macro::TokenStream {
222    let Some(env_var) = read_literal_str(tokens) else {
223        panic!("{INCLUDE_ERROR}");
224    };
225    let file_path = std::env::var(env_var).expect("Environment variable must exists.");
226
227    let data = match fs::read(file_path) {
228        Ok(data) => data,
229        Err(err) => panic!("Failed to read the file: {err}"),
230    };
231
232    LiteralBytes::ByteStr(data).encrypt().into()
233}
234
235/// Reads a single string literal from a token stream
236///
237/// # Arguments
238///
239/// * `tokens` - Token stream containing a single string literal
240fn read_literal_str(tokens: proc_macro::TokenStream) -> Option<String> {
241    if let Ok(expr) = syn::parse2::<Expr>(tokens.into()) {
242        match expr {
243            Expr::Lit(ExprLit {
244                lit: Lit::Str(str), ..
245            }) => return Some(str.value()),
246            _ => {}
247        }
248    };
249
250    None
251}
252
253/// Makes a path relative to the calling source code file
254fn relative_path(path: &str) -> PathBuf {
255    let current_dir = Path::new(file!())
256        .parent()
257        .unwrap_or_else(|| Path::new("."));
258
259    current_dir.join(path)
260}