ncr_crypto/
lib.rs

1//! # No Chat Reports (NCR) Crypto
2//!
3//! The cryptography used to generate passwords and encrypted messages
4//! exactly as the [No Chat Reports](https://github.com/Aizistral-Studios/No-Chat-Reports) Mod for Minecraft does.
5//!
6//! # Examples
7//!
8//! ```
9//! use base64::{alphabet::Alphabet, engine::{GeneralPurpose, GeneralPurposeConfig}, Engine};
10//! use ncr_crypto::{decrypt_with_passphrase, decode_and_verify};
11//!
12//! let passphrase = b"secret";  // Setting in NCR
13//! // "Hello, world!" sent as a message in chat:
14//! let alphabet = Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+\\").unwrap();
15//! let b64 = GeneralPurpose::new(&alphabet, GeneralPurposeConfig::new());
16//! let ciphertext = b64.decode("q2JCS\\M3yMnz+MtXDn4dd6xyqN94Dao=").unwrap();
17//!
18//! let decrypted = decrypt_with_passphrase(&ciphertext, passphrase);
19//! let decoded = decode_and_verify(&decrypted);
20//!
21//! assert_eq!(decoded, Ok("#%Hello, world!"))
22//! ```
23//!
24//! # How it works
25//!
26//! From reading the Source Code on Github it becomes clear how the mod does encryption:
27//!
28//! 1. You set a passphrase like "secret" in the UI
29//! 2. The mod uses `PBKDF2_HMAC_SHA1` with a hardcoded salt and 65536 iterations to make your passphrase
30//! into a hash of 16 bytes. This process takes the longest
31//! 3. An Initialization Vector (IV) is generated from a random nonce value, and used in the encryption that follows
32//! 4. The new hash becomes the key used for encrypting any messages you send with `AES-CFB8` encryption
33//! 5. The ciphertext that comes from this encryption is appended to the nonce that was generated, and the final message
34//! that is sent in Base64 encoding through the chat (note: `"#%"` is added as a prefix to the message before encrypting)
35//!
36//! Decrypting then is very similar, just in reverse:
37//!
38//! 1. Decode the message from Base64 into raw bytes
39//! 2. Get the nonce from the message and generate the IV again with it
40//! 2. Generate the hash from the secret passphrase again, and use it as the key for the AES encryption
41//! 3. If the decrypted message starts with `"#%"`, the rest is printed decrypted in the chat
42
43use std::{
44    error::Error,
45    fmt::Display,
46    io::{BufReader, Read},
47    str::from_utf8,
48};
49
50use aes::cipher::{AsyncStreamCipher, KeyIvInit};
51use bytes::{Buf, BufMut, Bytes, BytesMut};
52
53#[cfg(not(windows))]
54use fastpbkdf2::pbkdf2_hmac_sha1;
55
56#[cfg(windows)]
57use ring::pbkdf2;
58#[cfg(windows)]
59use ring::pbkdf2::PBKDF2_HMAC_SHA1;
60#[cfg(windows)]
61use std::num::NonZeroU32;
62
63type Aes128Cfb8Dec = cfb8::Decryptor<aes::Aes128>;
64type Aes128Cfb8Enc = cfb8::Encryptor<aes::Aes128>;
65
66/// Content salt for all passphrases ([source](https://github.com/Aizistral-Studios/No-Chat-Reports/blob/c2c60a03544952fe608bd65163cc0b2658e3c032/src/main/java/com/aizistral/nochatreports/encryption/AESEncryption.java#L57-L58))
67///
68/// Generated as follows:
69///
70/// ```
71/// let mut salt = [0; 16];
72/// java_rand::Random::new(1738389128127)
73///     .next_bytes(&mut salt);
74///
75/// assert_eq!(salt, [45, 72, 24, 73, 11, 12, 10, 149, 250, 165, 68, 71, 1, 217, 153, 119]);
76/// ```
77pub const SALT: [u8; 16] = [
78    45, 72, 24, 73, 11, 12, 10, 149, 250, 165, 68, 71, 1, 217, 153, 119,
79];
80
81/// Generate a key from a passphrase
82///
83/// Use `PBKDF2_HMAC_SHA1` with a hardcoded salt and 65536 iterations to hash a passphrase into a 16-byte key
84///
85/// # Examples
86///
87/// ```
88/// use base64::{alphabet::Alphabet, engine::{GeneralPurpose, GeneralPurposeConfig}, Engine};
89/// use ncr_crypto::generate_key;
90///
91/// let passphrase = b"secret";
92///
93/// let key = generate_key(passphrase);
94/// let alphabet = Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+\\").unwrap();
95/// let b64 = GeneralPurpose::new(&alphabet, GeneralPurposeConfig::new());
96///
97/// assert_eq!(b64.encode(key), "474esvGYVuN83HpxbK1uFQ==");  // Can be seen in the NCR UI when typing the passphrase
98/// ```
99pub fn generate_key(passphrase: &[u8]) -> [u8; 16] {
100    let mut key = [0; 16];
101
102    #[cfg(not(windows))]
103    pbkdf2_hmac_sha1(passphrase, &SALT, 65536, &mut key);
104
105    #[cfg(windows)]
106    pbkdf2::derive(
107        PBKDF2_HMAC_SHA1,
108        NonZeroU32::new(65536).unwrap(),
109        &SALT,
110        passphrase,
111        &mut key,
112    );
113
114    key
115}
116
117/// Encrypt a plaintext message with a given key
118///
119/// > **Warning**: This function does **not** append `"#%"` to the message before encrypting. NCR automatically does this when sending a message,
120/// > so add it if you're planning to send a real message that NCR should recognize
121///
122/// NCR uses AES-CFB8 for encryption, with a 16-byte key. Generate a key from [`generate_key()`] using a passphrase, or
123/// provide the raw bytes to this function. You can also use [`encrypt_with_passphrase()`] as a shorthand for doing both of these things
124///
125/// # Examples
126///
127/// ```
128/// use base64::{alphabet::Alphabet, engine::{GeneralPurpose, GeneralPurposeConfig}, Engine};
129/// use ncr_crypto::{encrypt, decrypt};
130///
131/// let alphabet = Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+\\").unwrap();
132/// let b64 = GeneralPurpose::new(&alphabet, GeneralPurposeConfig::new());
133/// let key = b64.decode("blfrngArk3chG6wzncOZ5A==").unwrap();  // Default key
134/// let key = key.try_into().unwrap();
135/// let plaintext = b"#%Hello, world!";
136///
137/// let encrypted = encrypt(plaintext, &key);
138/// // Here `encrypted` is something random like [240, 28, 167, ..., 237, 3, 89]
139/// let decrypted = decrypt(&encrypted, &key);
140///
141/// assert_eq!(decrypted, plaintext);
142/// ```
143pub fn encrypt(plaintext: &[u8], key: &[u8; 16]) -> Vec<u8> {
144    // This function is a bit of a mess with Bytes and conversions, but it works ¯\_(ツ)_/¯
145
146    let mut plaintext = Vec::from(plaintext);
147
148    let mut nonce = [0; 8];
149    getrandom::getrandom(&mut nonce).unwrap(); // Actually uses java.security.SecureRandom, but for generating our nonce this doesn't matter
150    let nonce = Bytes::from(Vec::from(nonce)).get_u64();
151
152    let mut iv = [0; 16];
153    java_rand::Random::new(nonce).next_bytes(&mut iv);
154
155    Aes128Cfb8Enc::new(key.into(), &iv.into()).encrypt(&mut plaintext);
156
157    let mut ciphertext = BytesMut::with_capacity(8 + plaintext.len());
158    ciphertext.put(Bytes::from(Vec::from(nonce.to_be_bytes())));
159    ciphertext.put(Bytes::from(plaintext)); // `plaintext` is encrypted at this point
160
161    ciphertext.to_vec()
162}
163
164/// Decrypt a ciphertext message with a given key
165///
166/// NCR uses AES-CFB8 for encryption, with a 16-byte key. Generate a key from [`generate_key()`] using a passphrase, or
167/// provide the raw bytes to this function. You can also use [`decrypt_with_passphrase()`] as a shorthand for doing both of these things
168///
169/// # Examples
170///
171/// ```
172/// use base64::{alphabet::Alphabet, engine::{GeneralPurpose, GeneralPurposeConfig}, Engine};
173/// use ncr_crypto::decrypt;
174///
175/// let alphabet = Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+\\").unwrap();
176/// let b64 = GeneralPurpose::new(&alphabet, GeneralPurposeConfig::new());
177/// let key = b64.decode("blfrngArk3chG6wzncOZ5A==").unwrap();  // Default key
178/// let ciphertext = b64.decode("NuhaeyIn3WJDHY\\W0X++EJKON32pDAA=").unwrap();
179///
180/// let decrypted = decrypt(&ciphertext, &key.try_into().unwrap());
181///
182/// assert_eq!(std::str::from_utf8(&decrypted).unwrap(), "#%Hello, world!");
183/// ```
184pub fn decrypt(ciphertext: &[u8], key: &[u8; 16]) -> Vec<u8> {
185    let mut buf_reader = BufReader::new(ciphertext);
186
187    let mut nonce = [0; 8];
188    buf_reader.read(&mut nonce).unwrap();
189    let nonce = u64::from_be_bytes(nonce);
190
191    let mut encrypted = Vec::new();
192    buf_reader.read_to_end(&mut encrypted).unwrap();
193
194    let mut iv = [0; 16];
195    java_rand::Random::new(nonce).next_bytes(&mut iv);
196
197    Aes128Cfb8Dec::new(key.into(), &iv.into()).decrypt(&mut encrypted);
198
199    encrypted.to_vec()
200}
201
202/// Encrypt a ciphertext message with a given passphrase
203///
204/// Shorthand for [`generate_key()`] and then [`encrypt()`]
205///
206/// # Examples
207///
208/// ```
209/// use ncr_crypto::{encrypt_with_passphrase, decrypt_with_passphrase};
210///
211/// let passphrase = b"secret";
212/// let plaintext = b"#%Hello, world!";
213///
214/// let encrypted = encrypt_with_passphrase(plaintext, passphrase);
215/// // Here `encrypted` is something random like [240, 28, 167, ..., 237, 3, 89]
216/// let decrypted = decrypt_with_passphrase(&encrypted, passphrase);
217///
218/// assert_eq!(decrypted, plaintext);
219/// ```
220pub fn encrypt_with_passphrase(plaintext: &[u8], passphrase: &[u8]) -> Vec<u8> {
221    let key = generate_key(passphrase);
222    encrypt(plaintext, &key)
223}
224
225/// Decrypt a ciphertext message with a given passphrase
226///
227/// Shorthand for [`generate_key()`] and then [`decrypt()`]
228///
229/// # Examples
230///
231/// ```
232/// use base64::{alphabet::Alphabet, engine::{GeneralPurpose, GeneralPurposeConfig}, Engine};
233/// use ncr_crypto::decrypt_with_passphrase;
234///
235/// let alphabet = Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+\\").unwrap();
236/// let b64 = GeneralPurpose::new(&alphabet, GeneralPurposeConfig::new());
237/// let passphrase = b"secret";
238/// let ciphertext = b64.decode("q2JCS\\M3yMnz+MtXDn4dd6xyqN94Dao=").unwrap();
239///
240/// let decrypted = decrypt_with_passphrase(&ciphertext, passphrase);
241///
242/// assert_eq!(std::str::from_utf8(&decrypted).unwrap(), "#%Hello, world!");
243/// ```
244pub fn decrypt_with_passphrase(ciphertext: &[u8], passphrase: &[u8]) -> Vec<u8> {
245    let key = generate_key(passphrase);
246    decrypt(ciphertext, &key)
247}
248
249#[derive(Debug, Clone, PartialEq)]
250pub struct FormatError;
251
252impl Error for FormatError {}
253impl Display for FormatError {
254    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
255        write!(f, "string did not start with '#%' or is invalid UTF8")
256    }
257}
258
259/// Verify if a message could be correctly decrypted
260///
261/// Decrypted message from NCR are always prefixed with "#%", and contain valid UTF8. This function verifies both of these things
262/// and returns a Result containing the decoded `&str` or a `FormatError` in case it is not valid
263///
264/// # Examples
265///
266/// ```
267/// use base64::{alphabet::Alphabet, engine::{GeneralPurpose, GeneralPurposeConfig}, Engine};
268/// use ncr_crypto::{decode_and_verify, decrypt_with_passphrase};
269///
270/// let alphabet = Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+\\").unwrap();
271/// let b64 = GeneralPurpose::new(&alphabet, GeneralPurposeConfig::new());
272/// let passphrase = b"secret";
273/// let ciphertext = b64.decode("q2JCS\\M3yMnz+MtXDn4dd6xyqN94Dao=").unwrap();
274///
275/// let decrypted = decrypt_with_passphrase(&ciphertext, passphrase);
276/// let decoded = decode_and_verify(&decrypted);
277///
278/// assert_eq!(decoded, Ok("#%Hello, world!"));
279/// ```
280///
281/// ```
282/// use base64::{alphabet::Alphabet, engine::{GeneralPurpose, GeneralPurposeConfig}, Engine};
283/// use ncr_crypto::{decode_and_verify, decrypt_with_passphrase, FormatError};
284///
285/// let alphabet = Alphabet::new("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+\\").unwrap();
286/// let b64 = GeneralPurpose::new(&alphabet, GeneralPurposeConfig::new());
287/// let passphrase = b"wrong";  // Should be "secret"
288/// let ciphertext = b64.decode("q2JCS\\M3yMnz+MtXDn4dd6xyqN94Dao=").unwrap();
289///
290/// let decrypted = decrypt_with_passphrase(&ciphertext, passphrase);
291/// let decoded = decode_and_verify(&decrypted);
292///
293/// assert_eq!(decoded, Err(FormatError));
294/// ```
295///
296/// ```
297/// use ncr_crypto::{decode_and_verify, FormatError};
298///
299/// let bytes = b"Hello, world!";  // Without "#%" prefix
300/// let decoded = decode_and_verify(bytes);
301///
302/// assert_eq!(decoded, Err(FormatError));
303/// ```
304pub fn decode_and_verify(bytes: &[u8]) -> Result<&str, FormatError> {
305    if bytes.len() < 2 || bytes[..2] != [35, 37] {
306        // "#%"
307        return Err(FormatError);
308    }
309
310    match from_utf8(bytes) {
311        Ok(decoded) => Ok(decoded),
312        Err(_) => Err(FormatError),
313    }
314}