obfstr/
bytes.rs

1/*!
2Byte string obfuscation
3=======================
4*/
5
6use core::ptr::{read_volatile, write};
7
8/// Compiletime string constant obfuscation.
9///
10/// The purpose of the obfuscation is to make it difficult to discover the original strings with automated analysis.
11/// String obfuscation is not intended to hinder a dedicated reverse engineer from discovering the original string.
12/// This should not be used to hide secrets in client binaries and the author disclaims any responsibility for any damages resulting from ignoring this warning.
13///
14/// The `obfstr!` macro returns the deobfuscated string as a temporary `&str` value and must be consumed in the same statement it was used:
15///
16/// ```
17/// use obfstr::obfstr as s;
18///
19/// const HELLO_WORLD: &str = "Hello 🌍";
20/// assert_eq!(s!(HELLO_WORLD), HELLO_WORLD);
21/// ```
22///
23/// Different syntax forms are supported to reuse the obfuscated strings in outer scopes:
24///
25/// ```
26/// use obfstr::obfstr as s;
27///
28/// // Obfuscate a bunch of strings
29/// s! {
30/// 	let s = "Hello world";
31/// 	let another = "another";
32/// }
33/// assert_eq!(s, "Hello world");
34/// assert_eq!(another, "another");
35///
36/// // Assign to an uninit variable in outer scope
37/// let (true_string, false_string);
38/// let string = if true {
39/// 	s!(true_string = "true")
40/// }
41/// else {
42/// 	s!(false_string = "false")
43/// };
44/// assert_eq!(string, "true");
45///
46/// // Return an obfuscated string from a function
47/// fn helper(buf: &mut [u8]) -> &str {
48/// 	s!(buf <- "hello")
49/// }
50/// let mut buf = [0u8; 16];
51/// assert_eq!(helper(&mut buf), "hello");
52/// ```
53#[macro_export]
54macro_rules! obfstr {
55	($(let $name:ident = $s:expr;)*) => {$(
56		$crate::obfbytes! { let $name = ::core::convert::identity::<&str>($s).as_bytes(); }
57		let $name = $crate::unsafe_as_str($name);
58	)*};
59	($name:ident = $s:expr) => {
60		$crate::unsafe_as_str($crate::obfbytes!($name = ::core::convert::identity::<&str>($s).as_bytes()))
61	};
62	($buf:ident <- $s:expr) => {
63		$crate::unsafe_as_str($crate::obfbytes!($buf <- ::core::convert::identity::<&str>($s).as_bytes()))
64	};
65	($s:expr) => {
66		$crate::unsafe_as_str($crate::obfbytes!(::core::convert::identity::<&str>($s).as_bytes()))
67	};
68}
69
70/// Compiletime cstr constant obfuscation.
71///
72/// See [`obfstr!`] for more information.
73///
74/// ```
75/// use std::ffi::CStr;
76/// use obfstr::obfcstr as cstr;
77///
78/// const HELLO_WORLD: &'static CStr = c"Hello CStr";
79/// assert_eq!(cstr!(HELLO_WORLD).to_str().unwrap(), "Hello CStr");
80/// ```
81#[macro_export]
82macro_rules! obfcstr {
83	($(let $name:ident = $s:expr;)*) => {$(
84		$crate::obfbytes! { let $name = ::core::ffi::CStr::to_bytes_with_nul($s); }
85		let $name = $crate::unsafe_as_cstr($name);
86	)*};
87	($name:ident = $s:expr) => {
88		$crate::unsafe_as_cstr($crate::obfbytes!($name = ::core::ffi::CStr::to_bytes_with_nul($s)))
89	};
90	($buf:ident <- $s:expr) => {
91		$crate::unsafe_as_cstr($crate::obfbytes!($buf <- ::core::ffi::CStr::to_bytes_with_nul($s)))
92	};
93	($s:expr) => {
94		$crate::unsafe_as_cstr($crate::obfbytes!(::core::ffi::CStr::to_bytes_with_nul($s)))
95	};
96}
97
98/// Compiletime string constant obfuscation.
99///
100/// Returns an owned `String` instead of a temporary `&str`.
101///
102/// See [`obfstr!`] for more information.
103#[macro_export]
104macro_rules! obfstring {
105	($s:expr) => {
106		String::from($crate::obfstr!($s))
107	};
108}
109
110/// Compiletime byte string obfuscation.
111#[macro_export]
112macro_rules! obfbytes {
113	($(let $name:ident = $s:expr;)*) => {
114		$(let ref $name = $crate::__obfbytes!($s);)*
115	};
116	($name:ident = $s:expr) => {{
117		$name = $crate::__obfbytes!($s);
118		&$name
119	}};
120	($buf:ident <- $s:expr) => {{
121		let buf = &mut $buf[..$s.len()];
122		buf.copy_from_slice(&$crate::__obfbytes!($s));
123		buf
124	}};
125	($s:expr) => {
126		&$crate::__obfbytes!($s)
127	};
128}
129
130#[doc(hidden)]
131#[macro_export]
132macro_rules! __obfbytes {
133	($s:expr) => {{
134		const _OBFBYTES_STRING: &[u8] = $s;
135		const _OBFBYTES_LEN: usize = _OBFBYTES_STRING.len();
136		const _OBFBYTES_KEYSTREAM: [u8; _OBFBYTES_LEN] = $crate::bytes::keystream::<_OBFBYTES_LEN>($crate::random!(u32, "key", stringify!($s)));
137		static _OBFBYTES_SDATA: [u8; _OBFBYTES_LEN] = $crate::bytes::obfuscate::<_OBFBYTES_LEN>(_OBFBYTES_STRING, &_OBFBYTES_KEYSTREAM);
138		$crate::bytes::deobfuscate::<_OBFBYTES_LEN>(
139			$crate::xref::xref::<_,
140				{$crate::random!(u32, "offset", stringify!($s))},
141				{$crate::random!(u64, "xref", stringify!($s))}>
142				(&_OBFBYTES_SDATA),
143			&_OBFBYTES_KEYSTREAM)
144	}};
145}
146
147// Simple XorShift to generate the key stream.
148// Security doesn't matter, we just want a number of random-looking bytes.
149#[inline(always)]
150const fn next_round(mut x: u32) -> u32 {
151	x ^= x << 13;
152	x ^= x >> 17;
153	x ^= x << 5;
154	return x;
155}
156
157/// Generate the key stream for array of given length.
158#[inline(always)]
159pub const fn keystream<const LEN: usize>(key: u32) -> [u8; LEN] {
160	let mut keys = [0u8; LEN];
161	let mut round_key = key;
162	let mut i = 0;
163	// Calculate the key stream in chunks of 4 bytes
164	while i < LEN & !3 {
165		round_key = next_round(round_key);
166		let kb = round_key.to_ne_bytes();
167		keys[i + 0] = kb[0];
168		keys[i + 1] = kb[1];
169		keys[i + 2] = kb[2];
170		keys[i + 3] = kb[3];
171		i += 4;
172	}
173	// Calculate the remaining bytes of the key stream
174	round_key = next_round(round_key);
175	let kb = round_key.to_ne_bytes();
176	match LEN % 4 {
177		1 => {
178			keys[i + 0] = kb[0];
179		},
180		2 => {
181			keys[i + 0] = kb[0];
182			keys[i + 1] = kb[1];
183		},
184		3 => {
185			keys[i + 0] = kb[0];
186			keys[i + 1] = kb[1];
187			keys[i + 2] = kb[2];
188		},
189		_ => (),
190	}
191	return keys;
192}
193
194/// Obfuscates the input string and given key stream.
195#[inline(always)]
196pub const fn obfuscate<const LEN: usize>(s: &[u8], k: &[u8; LEN]) -> [u8; LEN] {
197	if s.len() != LEN {
198		panic!("input string len not equal to key stream len");
199	}
200	let mut data = [0u8; LEN];
201	let mut i = 0usize;
202	while i < LEN {
203		data[i] = s[i] ^ k[i];
204		i += 1;
205	}
206	return data;
207}
208
209/// Deobfuscates the obfuscated input string and given key stream.
210#[inline(always)]
211pub fn deobfuscate<const LEN: usize>(s: &[u8; LEN], k: &[u8; LEN]) -> [u8; LEN] {
212	let mut buf = [0u8; LEN];
213	let mut i = 0;
214	// Try to tickle the LLVM optimizer in _just_ the right way
215	// Use `read_volatile` to avoid constant folding a specific read and optimize the rest
216	// Volatile reads of any size larger than 8 bytes appears to cause a bunch of one byte reads
217	// Hand optimize in chunks of 8 and 4 bytes to avoid this
218	unsafe {
219		let src = s.as_ptr();
220		let dest = buf.as_mut_ptr();
221		// Process in chunks of 8 bytes on 64-bit targets
222		#[cfg(target_pointer_width = "64")]
223		while i < LEN & !7 {
224			let ct = read_volatile(src.offset(i as isize) as *const [u8; 8]);
225			let tmp = u64::from_ne_bytes([ct[0], ct[1], ct[2], ct[3], ct[4], ct[5], ct[6], ct[7]]) ^
226				u64::from_ne_bytes([k[i + 0], k[i + 1], k[i + 2], k[i + 3], k[i + 4], k[i + 5], k[i + 6], k[i + 7]]);
227			write(dest.offset(i as isize) as *mut [u8; 8], tmp.to_ne_bytes());
228			i += 8;
229		}
230		// Process in chunks of 4 bytes
231		while i < LEN & !3 {
232			let ct = read_volatile(src.offset(i as isize) as *const [u8; 4]);
233			let tmp = u32::from_ne_bytes([ct[0], ct[1], ct[2], ct[3]]) ^
234				u32::from_ne_bytes([k[i + 0], k[i + 1], k[i + 2], k[i + 3]]);
235			write(dest.offset(i as isize) as *mut [u8; 4], tmp.to_ne_bytes());
236			i += 4;
237		}
238		// Process the remaining bytes
239		match LEN % 4 {
240			1 => {
241				let ct = read_volatile(src.offset(i as isize));
242				write(dest.offset(i as isize), ct ^ k[i]);
243			},
244			2 => {
245				let ct = read_volatile(src.offset(i as isize) as *const [u8; 2]);
246				write(dest.offset(i as isize) as *mut [u8; 2], [
247					ct[0] ^ k[i + 0],
248					ct[1] ^ k[i + 1],
249				]);
250			},
251			3 => {
252				let ct = read_volatile(src.offset(i as isize) as *const [u8; 3]);
253				write(dest.offset(i as isize) as *mut [u8; 2], [
254					ct[0] ^ k[i + 0],
255					ct[1] ^ k[i + 1],
256				]);
257				write(dest.offset(i as isize + 2), ct[2] ^ k[i + 2]);
258			},
259			_ => (),
260		}
261	}
262	return buf;
263}
264
265#[inline(always)]
266pub fn equals<const LEN: usize>(s: &[u8; LEN], k: &[u8; LEN], other: &[u8]) -> bool {
267	if other.len() != LEN {
268		return false;
269	}
270	let mut i = 0;
271	// Try to tickle the LLVM optimizer in _just_ the right way
272	// Use `read_volatile` to avoid constant folding a specific read and optimize the rest
273	// Volatile reads of any size larger than 8 bytes appears to cause a bunch of one byte reads
274	// Hand optimize in chunks of 8 and 4 bytes to avoid this
275	unsafe {
276		let src = s.as_ptr();
277		// Process in chunks of 8 bytes on 64-bit targets
278		#[cfg(target_pointer_width = "64")]
279		while i < LEN & !7 {
280			let ct = read_volatile(src.offset(i as isize) as *const [u8; 8]);
281			let tmp = u64::from_ne_bytes([ct[0], ct[1], ct[2], ct[3], ct[4], ct[5], ct[6], ct[7]]) ^
282				u64::from_ne_bytes([k[i + 0], k[i + 1], k[i + 2], k[i + 3], k[i + 4], k[i + 5], k[i + 6], k[i + 7]]);
283			let other = u64::from_ne_bytes([other[i + 0], other[i + 1], other[i + 2], other[i + 3], other[i + 4], other[i + 5], other[i + 6], other[i + 7]]);
284			if tmp != other {
285				return false;
286			}
287			i += 8;
288		}
289		// Process in chunks of 4 bytes
290		while i < LEN & !3 {
291			let ct = read_volatile(src.offset(i as isize) as *const [u8; 4]);
292			let tmp = u32::from_ne_bytes([ct[0], ct[1], ct[2], ct[3]]) ^
293				u32::from_ne_bytes([k[i + 0], k[i + 1], k[i + 2], k[i + 3]]);
294			let other = u32::from_ne_bytes([other[i + 0], other[i + 1], other[i + 2], other[i + 3]]);
295			if tmp != other {
296				return false;
297			}
298			i += 4;
299		}
300		// Process the remaining bytes
301		match LEN % 4 {
302			1 => {
303				let ct = read_volatile(src.offset(i as isize));
304				ct ^ k[i] == other[i]
305			},
306			2 => {
307				let ct = read_volatile(src.offset(i as isize) as *const [u8; 2]);
308				u16::from_ne_bytes([ct[0], ct[1]]) ^ u16::from_ne_bytes([k[i + 0], k[i + 1]]) == u16::from_ne_bytes([other[i + 0], other[i + 1]])
309			},
310			3 => {
311				let ct = read_volatile(src.offset(i as isize) as *const [u8; 3]);
312				u32::from_ne_bytes([ct[0], ct[1], ct[2], 0]) ^ u32::from_ne_bytes([k[i + 0], k[i + 1], k[i + 2], 0]) == u32::from_ne_bytes([other[i + 0], other[i + 1], other[i + 2], 0])
313			},
314			_ => true,
315		}
316	}
317}
318
319// Test correct processing of less than multiple of 8 lengths
320#[test]
321fn test_remaining_bytes() {
322	const STRING: &[u8] = b"01234567ABCDEFGHI";
323	fn test<const LEN: usize>(key: u32) {
324		let keys = keystream::<LEN>(key);
325		let data = obfuscate::<LEN>(&STRING[..LEN], &keys);
326		let buffer = deobfuscate::<LEN>(&data, &keys);
327		// Ciphertext should not equal input string
328		assert_ne!(&data[..], &STRING[..LEN]);
329		// Deobfuscated result should equal input string
330		assert_eq!(&buffer[..], &STRING[..LEN]);
331		// Specialized equals check should succeed
332		assert!(equals::<LEN>(&data, &keys, &STRING[..LEN]));
333	}
334	test::<8>(0x1111);
335	test::<9>(0x2222);
336	test::<10>(0x3333);
337	test::<11>(0x4444);
338	test::<12>(0x5555);
339	test::<13>(0x6666);
340	test::<14>(0x7777);
341	test::<15>(0x8888);
342	test::<16>(0x9999);
343}
344
345#[test]
346fn test_equals() {
347	const STRING: &str = "Hello 🌍";
348	const LEN: usize = STRING.len();
349	const KEYSTREAM: [u8; LEN] = keystream::<LEN>(0x10203040);
350	const OBFSTRING: [u8; LEN] = obfuscate::<LEN>(STRING.as_bytes(), &KEYSTREAM);
351	assert!(equals::<LEN>(&OBFSTRING, &KEYSTREAM, STRING.as_bytes()));
352}
353
354#[test]
355fn test_obfstr_let() {
356	obfstr! {
357		let abc = "abc";
358		let def = "defdef";
359	}
360	assert_eq!(abc, "abc");
361	assert_eq!(def, "defdef");
362}
363
364#[test]
365fn test_obfstr_const() {
366	assert_eq!(obfstr!("\u{20}\0"), " \0");
367	assert_eq!(obfstr!("\"\n\t\\\'\""), "\"\n\t\\\'\"");
368
369	const LONG_STRING: &str = "This literal is very very very long to see if it correctly handles long strings";
370	assert_eq!(obfstr!(LONG_STRING), LONG_STRING);
371
372	const ABC: &str = "ABC";
373	const WORLD: &str = "🌍";
374
375	assert_eq!(obfbytes!(ABC.as_bytes()), "ABC".as_bytes());
376	assert_eq!(obfbytes!(WORLD.as_bytes()), "🌍".as_bytes());
377}