Skip to main content

short_crypt/
lib.rs

1/*!
2# ShortCrypt
3
4ShortCrypt is a very simple deterministic encryption library, which aims to encrypt any data into something random at first glance.
5
6Even if these data are similar, the ciphers are still pretty different.
7
8The most important thing is that a cipher contains only **5 bits** more information than its plaintext so that it is suitable for data used in a URL or a QR Code. Besides these, it is also an ideal candidate for serial number generation.
9
10ShortCrypt does not provide cryptographic authentication and must not be used to protect sensitive data or resist malicious tampering.
11
12## Examples
13
14`encrypt` method can create a `Cipher` tuple separating into a **base** and a **body** of the cipher. The **base** contains 5 bits of information and is stored as a `u8`, while the size of the **body** is equal to the plaintext.
15
16```rust
17extern crate short_crypt;
18
19use short_crypt::ShortCrypt;
20
21let sc = ShortCrypt::new("magickey");
22
23assert_eq!((8, [216, 78, 214, 199, 157, 190, 78, 250].to_vec()), sc.encrypt("articles"));
24assert_eq!("articles".as_bytes().to_vec(), sc.decrypt(&(8, vec![216, 78, 214, 199, 157, 190, 78, 250])).unwrap());
25
26```
27
28`encrypt_to_url_component` method is common for encryption in most cases. After ShortCrypt `encrypt` a plaintext, it encodes the cipher into a random-like string based on Base64-URL format so that it can be concatenated with URLs.
29
30```rust
31extern crate short_crypt;
32
33use short_crypt::ShortCrypt;
34
35let sc = ShortCrypt::new("magickey");
36
37assert_eq!("2E87Wx52-Tvo", sc.encrypt_to_url_component("articles"));
38assert_eq!("articles".as_bytes().to_vec(), sc.decrypt_url_component("2E87Wx52-Tvo").unwrap());
39```
40
41`encrypt_to_qr_code_alphanumeric` method is usually used for encrypting something into a QR code. After ShortCrypt `encrypt` a plaintext, it encodes the cipher into a random-like string based on Base32 format so that it can be inserted into a QR code with the compatibility with alphanumeric mode.
42
43```rust
44extern crate short_crypt;
45
46use short_crypt::ShortCrypt;
47
48let sc = ShortCrypt::new("magickey");
49
50assert_eq!("3BHNNR45XZH8PU", sc.encrypt_to_qr_code_alphanumeric("articles"));
51assert_eq!("articles".as_bytes().to_vec(), sc.decrypt_qr_code_alphanumeric("3BHNNR45XZH8PU").unwrap());
52```
53
54Besides, in order to reduce the copy times of strings, you can also use `encrypt_to_url_component_and_push_to_string`, `encrypt_to_qr_code_alphanumeric_and_push_to_string` methods to use the same memory space.
55
56```rust
57extern crate short_crypt;
58
59use short_crypt::ShortCrypt;
60
61let sc = ShortCrypt::new("magickey");
62
63let url = "https://magiclen.org/".to_string();
64
65assert_eq!("https://magiclen.org/2E87Wx52-Tvo", sc.encrypt_to_url_component_and_push_to_string("articles", url));
66
67let url = "https://magiclen.org/".to_string();
68
69assert_eq!("https://magiclen.org/3BHNNR45XZH8PU", sc.encrypt_to_qr_code_alphanumeric_and_push_to_string("articles", url));
70```
71*/
72
73#![no_std]
74
75extern crate alloc;
76
77pub extern crate base32;
78pub extern crate base64_url;
79
80use alloc::{string::String, vec::Vec};
81use core::fmt::{self, Debug, Formatter};
82
83pub use base64_url::base64;
84use crc_any::{CRCu8, CRCu64};
85
86/// A tuple containing a 5-bit **base** stored as a `u8` and a **body** whose length equals the plaintext length. You can use your own algorithm to combine them, or use [`ShortCrypt::encrypt_to_url_component`] or [`ShortCrypt::encrypt_to_qr_code_alphanumeric`] to produce a random-like string.
87pub type Cipher = (u8, Vec<u8>);
88
89/// A deterministic encryption context derived from a string key.
90pub struct ShortCrypt {
91    hashed_key:  [u8; 8],
92    key_sum_rev: u64,
93}
94
95impl Debug for ShortCrypt {
96    #[inline]
97    fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), fmt::Error> {
98        f.debug_struct("ShortCrypt").finish_non_exhaustive()
99    }
100}
101
102#[inline]
103fn encode_base(base: u8) -> u8 {
104    debug_assert!(base < 32);
105
106    if base < 10 { base + b'0' } else { base - 10 + b'A' }
107}
108
109#[inline]
110fn decode_base(byte: u8) -> Option<u8> {
111    match byte {
112        b'0'..=b'9' => Some(byte - b'0'),
113        b'A'..=b'V' => Some(byte - b'A' + 10),
114        _ => None,
115    }
116}
117
118#[inline]
119fn checksum_base(data: &[u8]) -> u8 {
120    let mut crc8 = CRCu8::crc8cdma2000();
121
122    crc8.update(data);
123    crc8.get_crc() % 32
124}
125
126impl ShortCrypt {
127    /// Creates a new `ShortCrypt` instance from a string key.
128    pub fn new<S: AsRef<str>>(key: S) -> ShortCrypt {
129        let key_bytes = key.as_ref().as_bytes();
130
131        let hashed_key = {
132            let mut hasher = CRCu64::crc64we();
133
134            hasher.update(key_bytes);
135
136            hasher.get_crc().to_be_bytes()
137        };
138
139        let mut key_sum = 0u64;
140
141        for n in key_bytes.iter().copied() {
142            key_sum = key_sum.wrapping_add(u64::from(n));
143        }
144
145        let key_sum_rev = key_sum.reverse_bits();
146
147        ShortCrypt {
148            hashed_key,
149            key_sum_rev,
150        }
151    }
152
153    /// Encrypts byte-like plaintext into a [`Cipher`].
154    pub fn encrypt<T: ?Sized + AsRef<[u8]>>(&self, plaintext: &T) -> Cipher {
155        let data = plaintext.as_ref();
156
157        let len = data.len();
158
159        let base = checksum_base(data);
160
161        let mut encrypted = Vec::with_capacity(len);
162
163        let mut m = base;
164        let mut sum = u64::from(base);
165
166        for (i, d) in data.iter().enumerate() {
167            let offset = self.hashed_key[i % 8] ^ base;
168
169            let v = d ^ offset;
170
171            encrypted.push(v);
172
173            m ^= v;
174            sum = sum.wrapping_add(u64::from(v));
175        }
176
177        let sum: [u8; 8] = sum.to_be_bytes();
178
179        let hashed_array: [u8; 8] = {
180            let mut hasher = CRCu64::crc64we();
181
182            hasher.update(&[m]);
183            hasher.update(&sum);
184
185            hasher.get_crc().to_be_bytes()
186        };
187
188        for i in 0..len {
189            let index = i % 8;
190            let p = (hashed_array[index] ^ self.hashed_key[index]) as usize % len;
191
192            if i == p {
193                continue;
194            }
195
196            encrypted.swap(i, p);
197        }
198
199        (base, encrypted)
200    }
201
202    /// Decrypts a [`Cipher`] and rejects data whose 5-bit checksum does not match.
203    pub fn decrypt(&self, data: &Cipher) -> Result<Vec<u8>, &'static str> {
204        let base = data.0;
205        let data = &data.1;
206
207        if base > 31 {
208            return Err("The base is not correct.");
209        }
210
211        let mut decrypted = data.to_vec();
212
213        self.decrypt_appended_inner(base, 0, &mut decrypted)
214            .map_err(|_| "The cipher is incorrect.")?;
215
216        Ok(decrypted)
217    }
218
219    fn decrypt_appended_inner(
220        &self,
221        base: u8,
222        original_len: usize,
223        output: &mut Vec<u8>,
224    ) -> Result<(), ()> {
225        let len = output.len() - original_len;
226
227        let mut m = base;
228        let mut sum = u64::from(base);
229
230        for v in output[original_len..].iter().copied() {
231            m ^= v;
232            sum = sum.wrapping_add(u64::from(v));
233        }
234
235        let sum: [u8; 8] = sum.to_be_bytes();
236
237        let hashed_array: [u8; 8] = {
238            let mut hasher = CRCu64::crc64we();
239
240            hasher.update(&[m]);
241            hasher.update(&sum);
242
243            hasher.get_crc().to_be_bytes()
244        };
245
246        for i in (0..len).rev() {
247            let index = i % 8;
248            let p = (hashed_array[index] ^ self.hashed_key[index]) as usize % len;
249
250            if i == p {
251                continue;
252            }
253
254            output.swap(original_len + i, original_len + p);
255        }
256
257        for (i, d) in output[original_len..].iter_mut().enumerate() {
258            let offset = self.hashed_key[i % 8] ^ base;
259
260            *d ^= offset;
261        }
262
263        if checksum_base(&output[original_len..]) != base {
264            output.truncate(original_len);
265
266            return Err(());
267        }
268
269        Ok(())
270    }
271
272    fn extract_base(&self, bytes: &[u8]) -> Option<(u8, usize)> {
273        if bytes.is_empty() {
274            return None;
275        }
276
277        let mut sum = 0u64;
278
279        for n in bytes.iter().copied() {
280            sum = sum.wrapping_add(u64::from(n));
281        }
282
283        let base_index = ((self.key_sum_rev ^ sum) % (bytes.len() as u64)) as usize;
284        let base = decode_base(bytes[base_index])?;
285
286        Some((base, base_index))
287    }
288
289    fn insert_base(&self, base: u8, original_len: usize, mut output: String) -> String {
290        let base = encode_base(base);
291        let mut sum = u64::from(base);
292
293        for n in output.bytes().skip(original_len) {
294            sum = sum.wrapping_add(u64::from(n));
295        }
296
297        let base_index =
298            ((self.key_sum_rev ^ sum) % ((output.len() - original_len + 1) as u64)) as usize;
299
300        output.insert(original_len + base_index, base as char);
301        output
302    }
303
304    /// Encrypts data and encodes the cipher as a Base64-URL component.
305    #[inline]
306    pub fn encrypt_to_url_component<T: ?Sized + AsRef<[u8]>>(&self, data: &T) -> String {
307        self.encrypt_to_url_component_and_push_to_string(data, String::new())
308    }
309
310    /// Encrypts data and appends the Base64-URL component to a string.
311    pub fn encrypt_to_url_component_and_push_to_string<T: ?Sized + AsRef<[u8]>, S: Into<String>>(
312        &self,
313        data: &T,
314        output: S,
315    ) -> String {
316        let (base, encrypted) = self.encrypt(data);
317
318        let mut output = output.into();
319
320        let original_len = output.len();
321
322        base64_url::encode_to_string(&encrypted, &mut output);
323
324        self.insert_base(base, original_len, output)
325    }
326
327    /// Decodes and decrypts a Base64-URL component.
328    #[inline]
329    pub fn decrypt_url_component<S: AsRef<str>>(
330        &self,
331        url_component: S,
332    ) -> Result<Vec<u8>, &'static str> {
333        self.decrypt_url_component_and_push_to_vec(url_component, Vec::new())
334    }
335
336    /// Decodes and decrypts a Base64-URL component and appends the plaintext to a vector.
337    pub fn decrypt_url_component_and_push_to_vec<S: AsRef<str>>(
338        &self,
339        url_component: S,
340        mut output: Vec<u8>,
341    ) -> Result<Vec<u8>, &'static str> {
342        let bytes = url_component.as_ref().as_bytes();
343        let (base, base_index) =
344            self.extract_base(bytes).ok_or("The URL component is incorrect.")?;
345
346        let encrypted_base64_url = [&bytes[..base_index], &bytes[(base_index + 1)..]].concat();
347        let original_len = output.len();
348
349        base64_url::decode_to_vec(&encrypted_base64_url, &mut output)
350            .map_err(|_| "The URL component is incorrect.")?;
351
352        self.decrypt_appended_inner(base, original_len, &mut output)
353            .map_err(|_| "The URL component is incorrect.")?;
354
355        Ok(output)
356    }
357
358    /// Encrypts data and encodes the cipher as Base32 text for QR alphanumeric mode.
359    #[inline]
360    pub fn encrypt_to_qr_code_alphanumeric<T: ?Sized + AsRef<[u8]>>(&self, data: &T) -> String {
361        self.encrypt_to_qr_code_alphanumeric_and_push_to_string(data, String::new())
362    }
363
364    /// Encrypts data and appends Base32 text for QR alphanumeric mode to a string.
365    pub fn encrypt_to_qr_code_alphanumeric_and_push_to_string<
366        T: ?Sized + AsRef<[u8]>,
367        S: Into<String>,
368    >(
369        &self,
370        data: &T,
371        output: S,
372    ) -> String {
373        let (base, encrypted) = self.encrypt(data);
374
375        let mut output = output.into();
376
377        let original_len = output.len();
378
379        output.push_str(&base32::encode(
380            base32::Alphabet::Rfc4648 {
381                padding: false
382            },
383            &encrypted,
384        ));
385
386        self.insert_base(base, original_len, output)
387    }
388
389    /// Decodes and decrypts Base32 text produced for QR alphanumeric mode.
390    #[inline]
391    pub fn decrypt_qr_code_alphanumeric<S: AsRef<str>>(
392        &self,
393        qr_code_alphanumeric: S,
394    ) -> Result<Vec<u8>, &'static str> {
395        self.decrypt_qr_code_alphanumeric_and_push_to_vec(qr_code_alphanumeric, Vec::new())
396    }
397
398    /// Decodes and decrypts QR alphanumeric text and appends the plaintext to a vector.
399    pub fn decrypt_qr_code_alphanumeric_and_push_to_vec<S: AsRef<str>>(
400        &self,
401        qr_code_alphanumeric: S,
402        mut output: Vec<u8>,
403    ) -> Result<Vec<u8>, &'static str> {
404        let bytes = qr_code_alphanumeric.as_ref().as_bytes();
405        let (base, base_index) =
406            self.extract_base(bytes).ok_or("The QR code alphanumeric text is incorrect.")?;
407
408        let encrypted_base32 =
409            String::from_utf8([&bytes[..base_index], &bytes[(base_index + 1)..]].concat())
410                .map_err(|_| "The QR code alphanumeric text is incorrect.")?;
411
412        let encrypted = base32::decode(
413            base32::Alphabet::Rfc4648 {
414                padding: false
415            },
416            &encrypted_base32,
417        )
418        .ok_or("The QR code alphanumeric text is incorrect.")?;
419
420        let original_len = output.len();
421
422        if original_len == 0 {
423            output = encrypted;
424        } else {
425            output.extend_from_slice(&encrypted);
426        }
427
428        self.decrypt_appended_inner(base, original_len, &mut output)
429            .map_err(|_| "The QR code alphanumeric text is incorrect.")?;
430
431        Ok(output)
432    }
433}