Skip to main content

oxidized_crypto/
lib.rs

1//! Cryptographic primitives for the Minecraft protocol.
2//!
3//! Provides AES-128-CFB8 stream encryption (used after the login handshake),
4//! RSA-1024 key pair generation (for the server's identity during login),
5//! and RSA decryption (to unwrap the client's shared secret).
6
7#![warn(missing_docs)]
8#![deny(unsafe_code)]
9
10use aes::Aes128;
11use aes::cipher::{BlockEncrypt, KeyInit};
12use rsa::Pkcs1v15Encrypt;
13use rsa::pkcs8::EncodePublicKey;
14use rsa::{RsaPrivateKey, RsaPublicKey};
15use sha1::{Digest, Sha1};
16use thiserror::Error;
17
18// ---------------------------------------------------------------------------
19// Errors
20// ---------------------------------------------------------------------------
21
22/// Errors from cryptographic operations.
23#[derive(Debug, Error)]
24#[non_exhaustive]
25pub enum CryptoError {
26    /// RSA key generation failed.
27    #[error("RSA key generation failed: {0}")]
28    KeyGeneration(String),
29
30    /// RSA decryption failed (bad shared secret or challenge).
31    #[error("RSA decryption failed: {0}")]
32    Decryption(String),
33
34    /// The decrypted shared secret has the wrong length.
35    #[error("invalid shared secret length: expected 16, got {0}")]
36    InvalidSecretLength(usize),
37
38    /// RSA public key encoding failed.
39    #[error("public key encoding failed: {0}")]
40    PublicKeyEncoding(String),
41}
42
43// ---------------------------------------------------------------------------
44// RSA Key Pair
45// ---------------------------------------------------------------------------
46
47/// An RSA key pair for the Minecraft login handshake.
48///
49/// Generated once at server startup. The public key is sent to clients
50/// in `ClientboundHelloPacket`; the private key decrypts the client's
51/// shared secret.
52pub struct ServerKeyPair {
53    private_key: RsaPrivateKey,
54    public_key: RsaPublicKey,
55    /// DER-encoded public key (X.509 SubjectPublicKeyInfo) sent to clients.
56    public_key_der: Vec<u8>,
57}
58
59impl ServerKeyPair {
60    /// Generates a new 1024-bit RSA key pair.
61    ///
62    /// # Errors
63    ///
64    /// Returns [`CryptoError::KeyGeneration`] if the OS RNG or RSA
65    /// generation fails.
66    pub fn generate() -> Result<Self, CryptoError> {
67        let mut rng = rsa::rand_core::OsRng;
68        let private_key = RsaPrivateKey::new(&mut rng, 1024)
69            .map_err(|e| CryptoError::KeyGeneration(e.to_string()))?;
70        let public_key = RsaPublicKey::from(&private_key);
71        let public_key_der = public_key
72            .to_public_key_der()
73            .map_err(|e| CryptoError::PublicKeyEncoding(e.to_string()))?
74            .to_vec();
75
76        Ok(Self {
77            private_key,
78            public_key,
79            public_key_der,
80        })
81    }
82
83    /// Returns the DER-encoded public key (X.509 SubjectPublicKeyInfo).
84    pub fn public_key_der(&self) -> &[u8] {
85        &self.public_key_der
86    }
87
88    /// Returns a reference to the RSA public key.
89    pub fn public_key(&self) -> &RsaPublicKey {
90        &self.public_key
91    }
92
93    /// Decrypts data encrypted with the public key (PKCS#1 v1.5 padding).
94    ///
95    /// Used to decrypt both the shared secret and the verification challenge.
96    ///
97    /// # Errors
98    ///
99    /// Returns [`CryptoError::Decryption`] if decryption fails.
100    pub fn decrypt(&self, ciphertext: &[u8]) -> Result<Vec<u8>, CryptoError> {
101        self.private_key
102            .decrypt(Pkcs1v15Encrypt, ciphertext)
103            .map_err(|e| CryptoError::Decryption(e.to_string()))
104    }
105
106    /// Decrypts the client's shared secret and validates its length.
107    ///
108    /// The shared secret must be exactly 16 bytes (128-bit AES key).
109    ///
110    /// # Errors
111    ///
112    /// Returns [`CryptoError`] if decryption fails or the decrypted
113    /// secret is not 16 bytes.
114    pub fn decrypt_shared_secret(&self, encrypted_secret: &[u8]) -> Result<[u8; 16], CryptoError> {
115        let decrypted = self.decrypt(encrypted_secret)?;
116        decrypted
117            .try_into()
118            .map_err(|v: Vec<u8>| CryptoError::InvalidSecretLength(v.len()))
119    }
120}
121
122impl std::fmt::Debug for ServerKeyPair {
123    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
124        f.debug_struct("ServerKeyPair")
125            .field("public_key_der_len", &self.public_key_der.len())
126            .finish()
127    }
128}
129
130// ---------------------------------------------------------------------------
131// AES-128-CFB8 Cipher
132// ---------------------------------------------------------------------------
133
134/// Stateful AES-128-CFB8 stream cipher for Minecraft protocol encryption.
135///
136/// After the login handshake, all traffic is encrypted with AES-128-CFB8
137/// where the key and IV are both the 16-byte shared secret. The cipher
138/// is **stateful** — its internal feedback register advances with every
139/// byte, so encryption must happen on the raw TCP byte stream, not
140/// per-frame.
141///
142/// CFB-8 processes one byte at a time:
143/// 1. Encrypt the 16-byte shift register with AES-128-ECB
144/// 2. XOR the first byte of the result with the plaintext/ciphertext byte
145/// 3. Shift the register left by 1 byte, inserting the output byte at the end
146pub struct CipherState {
147    cipher: Aes128,
148    enc_iv: [u8; 16],
149    dec_iv: [u8; 16],
150}
151
152impl CipherState {
153    /// Creates a new cipher from the shared secret.
154    ///
155    /// Per the Minecraft protocol, the key and IV are both the 16-byte
156    /// shared secret (AES-128-CFB8 with key == IV).
157    pub fn new(shared_secret: &[u8; 16]) -> Self {
158        Self {
159            cipher: Aes128::new(shared_secret.into()),
160            enc_iv: *shared_secret,
161            dec_iv: *shared_secret,
162        }
163    }
164
165    /// Decrypts data in-place.
166    ///
167    /// Must be called on the raw byte stream (before frame decoding).
168    pub fn decrypt(&mut self, data: &mut [u8]) {
169        for byte in data.iter_mut() {
170            let mut block = self.dec_iv.into();
171            self.cipher.encrypt_block(&mut block);
172            let ciphertext_byte = *byte;
173            *byte ^= block[0];
174            // Shift register: drop first byte, append ciphertext byte
175            self.dec_iv.copy_within(1.., 0);
176            self.dec_iv[15] = ciphertext_byte;
177        }
178    }
179
180    /// Encrypts data in-place.
181    ///
182    /// Must be called on the raw byte stream (after frame encoding).
183    pub fn encrypt(&mut self, data: &mut [u8]) {
184        for byte in data.iter_mut() {
185            let mut block = self.enc_iv.into();
186            self.cipher.encrypt_block(&mut block);
187            *byte ^= block[0];
188            // Shift register: drop first byte, append ciphertext byte
189            self.enc_iv.copy_within(1.., 0);
190            self.enc_iv[15] = *byte;
191        }
192    }
193
194    /// Splits this cipher into independent decrypt and encrypt halves.
195    ///
196    /// Each half owns its own AES key schedule and IV register, allowing
197    /// concurrent decryption (reader task) and encryption (writer task)
198    /// after the connection is split.
199    pub fn split(self) -> (DecryptCipher, EncryptCipher) {
200        (
201            DecryptCipher {
202                cipher: self.cipher.clone(),
203                iv: self.dec_iv,
204            },
205            EncryptCipher {
206                cipher: self.cipher,
207                iv: self.enc_iv,
208            },
209        )
210    }
211}
212
213impl std::fmt::Debug for CipherState {
214    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
215        f.debug_struct("CipherState").finish()
216    }
217}
218
219// ---------------------------------------------------------------------------
220// Split cipher halves
221// ---------------------------------------------------------------------------
222
223/// Decrypt half of a split [`CipherState`].
224///
225/// Owns an independent AES key schedule and the decryption IV register.
226/// Used by the connection reader task after [`CipherState::split`].
227pub struct DecryptCipher {
228    cipher: Aes128,
229    iv: [u8; 16],
230}
231
232impl DecryptCipher {
233    /// Decrypts data in-place using AES-128-CFB8.
234    ///
235    /// Must be called on the raw byte stream (before frame decoding).
236    pub fn decrypt(&mut self, data: &mut [u8]) {
237        for byte in data.iter_mut() {
238            let mut block = self.iv.into();
239            self.cipher.encrypt_block(&mut block);
240            let ciphertext_byte = *byte;
241            *byte ^= block[0];
242            self.iv.copy_within(1.., 0);
243            self.iv[15] = ciphertext_byte;
244        }
245    }
246}
247
248impl std::fmt::Debug for DecryptCipher {
249    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
250        f.debug_struct("DecryptCipher").finish()
251    }
252}
253
254/// Encrypt half of a split [`CipherState`].
255///
256/// Owns an independent AES key schedule and the encryption IV register.
257/// Used by the connection writer task after [`CipherState::split`].
258pub struct EncryptCipher {
259    cipher: Aes128,
260    iv: [u8; 16],
261}
262
263impl EncryptCipher {
264    /// Encrypts data in-place using AES-128-CFB8.
265    ///
266    /// Must be called on the raw byte stream (after frame encoding).
267    pub fn encrypt(&mut self, data: &mut [u8]) {
268        for byte in data.iter_mut() {
269            let mut block = self.iv.into();
270            self.cipher.encrypt_block(&mut block);
271            *byte ^= block[0];
272            self.iv.copy_within(1.., 0);
273            self.iv[15] = *byte;
274        }
275    }
276}
277
278impl std::fmt::Debug for EncryptCipher {
279    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
280        f.debug_struct("EncryptCipher").finish()
281    }
282}
283
284// ---------------------------------------------------------------------------
285// Minecraft Auth Hash
286// ---------------------------------------------------------------------------
287
288/// Computes the Minecraft authentication hash (server ID digest).
289///
290/// This is the non-standard SHA-1 "twos-complement hex" encoding used by
291/// Mojang's session server. It computes:
292///
293/// ```text
294/// SHA1(server_id_bytes + shared_secret + public_key_der)
295/// ```
296///
297/// Then interprets the result as a signed big-endian integer and formats
298/// it as a lowercase hex string (with leading minus for negative values,
299/// no leading zeros).
300///
301/// Known test vectors from wiki.vg:
302/// - `"Notch"` → `"4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48"`
303/// - `"jeb_"` → `"-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1"`
304/// - `"simon"` → `"88e16a1019277b15b58571f3c71afe77e69d0bda"`
305pub fn minecraft_digest(server_id: &str, shared_secret: &[u8], public_key_der: &[u8]) -> String {
306    let mut hasher = Sha1::new();
307    hasher.update(server_id.as_bytes());
308    hasher.update(shared_secret);
309    hasher.update(public_key_der);
310    let hash = hasher.finalize();
311
312    // Interpret the 20-byte hash as a signed big-endian integer.
313    // If the high bit is set, the value is negative (twos-complement).
314    let negative = hash[0] & 0x80 != 0;
315
316    if negative {
317        // Negate: invert all bits, add 1
318        let mut bytes = hash.to_vec();
319        let mut carry = true;
320        for byte in bytes.iter_mut().rev() {
321            *byte = !*byte;
322            if carry {
323                let (result, overflow) = byte.overflowing_add(1);
324                *byte = result;
325                carry = overflow;
326            }
327        }
328        // Format as hex, strip leading zeros, prepend minus
329        let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
330        let trimmed = hex.trim_start_matches('0');
331        format!("-{trimmed}")
332    } else {
333        let hex: String = hash.iter().map(|b| format!("{b:02x}")).collect();
334        let trimmed = hex.trim_start_matches('0');
335        if trimmed.is_empty() {
336            "0".to_string()
337        } else {
338            trimmed.to_string()
339        }
340    }
341}
342
343/// Generates an offline-mode UUID v3 from a player name.
344///
345/// This matches the vanilla `UUIDUtil.createOfflineProfile()` which uses
346/// `UUID.nameUUIDFromBytes("OfflinePlayer:<name>")`. Java's
347/// `nameUUIDFromBytes` computes MD5 of the raw input bytes (no namespace
348/// prefix), then sets version=3 and IETF variant bits.
349pub fn offline_uuid(name: &str) -> uuid::Uuid {
350    use md5::{Digest as Md5Digest, Md5};
351
352    let input = format!("OfflinePlayer:{name}");
353    let hash = Md5::digest(input.as_bytes());
354    let mut bytes: [u8; 16] = hash.into();
355
356    // Set version 3 (name-based MD5)
357    bytes[6] = (bytes[6] & 0x0f) | 0x30;
358    // Set variant (IETF)
359    bytes[8] = (bytes[8] & 0x3f) | 0x80;
360
361    uuid::Uuid::from_bytes(bytes)
362}
363
364// ---------------------------------------------------------------------------
365// Verification challenge
366// ---------------------------------------------------------------------------
367
368/// Generates a random 4-byte verification challenge.
369///
370/// Vanilla uses `Ints.toByteArray(random.nextInt())`.
371pub fn generate_challenge() -> [u8; 4] {
372    use rand::RngExt;
373    let mut buf = [0u8; 4];
374    rand::rng().fill(&mut buf[..]);
375    buf
376}
377
378#[cfg(test)]
379#[allow(clippy::unwrap_used, clippy::expect_used)]
380mod tests {
381    use super::*;
382    use rsa::rand_core::OsRng;
383
384    // -----------------------------------------------------------------------
385    // AES-128-CFB8 cipher tests
386    // -----------------------------------------------------------------------
387
388    #[test]
389    fn test_cipher_encrypt_decrypt_roundtrip() {
390        let secret = [0x42u8; 16];
391        let mut cipher_enc = CipherState::new(&secret);
392        let mut cipher_dec = CipherState::new(&secret);
393
394        let original = b"Hello, Minecraft!".to_vec();
395        let mut data = original.clone();
396
397        cipher_enc.encrypt(&mut data);
398        assert_ne!(data, original, "encrypted data should differ from original");
399
400        cipher_dec.decrypt(&mut data);
401        assert_eq!(data, original, "decrypted data should match original");
402    }
403
404    #[test]
405    fn test_cipher_is_stateful() {
406        let secret = [0xAB; 16];
407        let mut cipher = CipherState::new(&secret);
408
409        let mut data1 = b"test".to_vec();
410        cipher.encrypt(&mut data1);
411
412        let mut data2 = b"test".to_vec();
413        cipher.encrypt(&mut data2);
414
415        assert_ne!(
416            data1, data2,
417            "same plaintext encrypted twice should differ (stateful cipher)"
418        );
419    }
420
421    #[test]
422    fn test_cipher_multi_chunk_roundtrip() {
423        let secret = [0x13; 16];
424        let mut enc = CipherState::new(&secret);
425        let mut dec = CipherState::new(&secret);
426
427        let chunk1 = b"first chunk ".to_vec();
428        let chunk2 = b"second chunk".to_vec();
429
430        let mut enc1 = chunk1.clone();
431        enc.encrypt(&mut enc1);
432
433        let mut enc2 = chunk2.clone();
434        enc.encrypt(&mut enc2);
435
436        dec.decrypt(&mut enc1);
437        dec.decrypt(&mut enc2);
438
439        assert_eq!(enc1, chunk1);
440        assert_eq!(enc2, chunk2);
441    }
442
443    #[test]
444    fn test_cipher_empty_data() {
445        let secret = [0x00; 16];
446        let mut cipher = CipherState::new(&secret);
447        let mut data = Vec::new();
448        cipher.encrypt(&mut data); // should not panic
449        cipher.decrypt(&mut data); // should not panic
450    }
451
452    // -----------------------------------------------------------------------
453    // RSA key pair tests
454    // -----------------------------------------------------------------------
455
456    #[test]
457    fn test_rsa_keygen_and_der() {
458        let keypair = ServerKeyPair::generate().expect("key generation");
459        assert!(
460            keypair.public_key_der().len() > 100,
461            "public key DER should be > 100 bytes"
462        );
463        assert!(
464            keypair.public_key_der().len() < 300,
465            "public key DER should be < 300 bytes"
466        );
467    }
468
469    #[test]
470    fn test_rsa_encrypt_decrypt_roundtrip() {
471        let keypair = ServerKeyPair::generate().expect("key generation");
472        let plaintext = b"shared_secret!!!"; // 16 bytes
473
474        let ciphertext = keypair
475            .public_key
476            .encrypt(&mut OsRng, Pkcs1v15Encrypt, plaintext)
477            .expect("encryption");
478
479        let decrypted = keypair.decrypt(&ciphertext).expect("decryption");
480        assert_eq!(decrypted, plaintext);
481    }
482
483    #[test]
484    fn test_decrypt_shared_secret_correct_length() {
485        let keypair = ServerKeyPair::generate().expect("key generation");
486        let secret = [0x42u8; 16];
487
488        let encrypted = keypair
489            .public_key
490            .encrypt(&mut OsRng, Pkcs1v15Encrypt, &secret)
491            .expect("encryption");
492
493        let decrypted = keypair
494            .decrypt_shared_secret(&encrypted)
495            .expect("decryption");
496        assert_eq!(decrypted, secret);
497    }
498
499    #[test]
500    fn test_decrypt_shared_secret_wrong_length() {
501        let keypair = ServerKeyPair::generate().expect("key generation");
502        let wrong = [0x42u8; 8]; // Only 8 bytes, not 16
503
504        let encrypted = keypair
505            .public_key
506            .encrypt(&mut OsRng, Pkcs1v15Encrypt, &wrong)
507            .expect("encryption");
508
509        let err = keypair.decrypt_shared_secret(&encrypted).unwrap_err();
510        assert!(matches!(err, CryptoError::InvalidSecretLength(8)));
511    }
512
513    // -----------------------------------------------------------------------
514    // Auth hash tests (wiki.vg test vectors)
515    // -----------------------------------------------------------------------
516
517    #[test]
518    fn test_minecraft_digest_notch() {
519        let result = minecraft_digest("Notch", &[], &[]);
520        assert_eq!(result, "4ed1f46bbe04bc756bcb17c0c7ce3e4632f06a48");
521    }
522
523    #[test]
524    fn test_minecraft_digest_jeb() {
525        let result = minecraft_digest("jeb_", &[], &[]);
526        assert_eq!(result, "-7c9d5b0044c130109a5d7b5fb5c317c02b4e28c1");
527    }
528
529    #[test]
530    fn test_minecraft_digest_simon() {
531        let result = minecraft_digest("simon", &[], &[]);
532        assert_eq!(result, "88e16a1019277b15d58faf0541e11910eb756f6");
533    }
534
535    // -----------------------------------------------------------------------
536    // Offline UUID tests
537    // -----------------------------------------------------------------------
538
539    #[test]
540    fn test_offline_uuid_deterministic() {
541        let uuid1 = offline_uuid("TestPlayer");
542        let uuid2 = offline_uuid("TestPlayer");
543        assert_eq!(uuid1, uuid2, "same name should produce same UUID");
544    }
545
546    #[test]
547    fn test_offline_uuid_different_names() {
548        let uuid1 = offline_uuid("Alice");
549        let uuid2 = offline_uuid("Bob");
550        assert_ne!(
551            uuid1, uuid2,
552            "different names should produce different UUIDs"
553        );
554    }
555
556    #[test]
557    fn test_offline_uuid_is_v3() {
558        let uuid = offline_uuid("Steve");
559        assert_eq!(
560            uuid.get_version(),
561            Some(uuid::Version::Md5),
562            "offline UUID should be version 3 (MD5)"
563        );
564    }
565
566    #[test]
567    fn test_offline_uuid_matches_java() {
568        let uuid = offline_uuid("Notch");
569        assert_eq!(
570            uuid.to_string(),
571            "b50ad385-829d-3141-a216-7e7d7539ba7f",
572            "offline UUID should match Java's UUID.nameUUIDFromBytes"
573        );
574    }
575
576    // -----------------------------------------------------------------------
577    // Challenge generation tests
578    // -----------------------------------------------------------------------
579
580    #[test]
581    fn test_generate_challenge_length() {
582        let challenge = generate_challenge();
583        assert_eq!(challenge.len(), 4);
584    }
585
586    #[test]
587    fn test_generate_challenge_random() {
588        let c1 = generate_challenge();
589        let c2 = generate_challenge();
590        assert_ne!(c1, c2, "two challenges should almost certainly differ");
591    }
592
593    // -----------------------------------------------------------------------
594    // CipherState::split tests
595    // -----------------------------------------------------------------------
596
597    #[test]
598    fn test_cipher_split_roundtrip() {
599        let secret = [0x42u8; 16];
600        let cipher = CipherState::new(&secret);
601        let (mut decrypt, mut encrypt) = cipher.split();
602
603        let original = b"Hello, split cipher!".to_vec();
604        let mut data = original.clone();
605
606        encrypt.encrypt(&mut data);
607        assert_ne!(data, original);
608
609        decrypt.decrypt(&mut data);
610        assert_eq!(data, original);
611    }
612
613    #[test]
614    fn test_cipher_split_matches_unsplit() {
615        let secret = [0x13u8; 16];
616
617        let mut unsplit = CipherState::new(&secret);
618        let original = b"consistency check".to_vec();
619        let mut data_unsplit = original.clone();
620        unsplit.encrypt(&mut data_unsplit);
621
622        let cipher = CipherState::new(&secret);
623        let (_dec, mut enc) = cipher.split();
624        let mut data_split = original;
625        enc.encrypt(&mut data_split);
626
627        assert_eq!(
628            data_unsplit, data_split,
629            "split cipher should produce identical ciphertext"
630        );
631    }
632
633    #[test]
634    fn test_cipher_split_multi_chunk() {
635        let secret = [0xAB; 16];
636        let cipher = CipherState::new(&secret);
637        let (mut decrypt, mut encrypt) = cipher.split();
638
639        let chunk1 = b"first chunk".to_vec();
640        let chunk2 = b"second chunk".to_vec();
641        let chunk3 = b"third chunk".to_vec();
642
643        let mut enc1 = chunk1.clone();
644        let mut enc2 = chunk2.clone();
645        let mut enc3 = chunk3.clone();
646
647        encrypt.encrypt(&mut enc1);
648        encrypt.encrypt(&mut enc2);
649        encrypt.encrypt(&mut enc3);
650
651        decrypt.decrypt(&mut enc1);
652        decrypt.decrypt(&mut enc2);
653        decrypt.decrypt(&mut enc3);
654
655        assert_eq!(enc1, chunk1);
656        assert_eq!(enc2, chunk2);
657        assert_eq!(enc3, chunk3);
658    }
659}