skrillax_security/lib.rs
1//! This module underpins the security aspects of the network communication between a Silkroad
2//! Online client and server. It provides the building blocks to establish a common encryption
3//! secret ([SilkroadEncryption]) through a handshake ([handshake::ActiveHandshake],
4//! [handshake::PassiveHandshake]), which als provide CRC and count security checks. This provided
5//! without any I/O, such that you could feasibly add it to whatever setup you have, or you can use
6//! the [skrillax-stream](https://docs.rs/skrillax-stream) crate that provides an async version on
7//! top of tokio (albeit with a bit more than just the handshake).
8//!
9//! The main way you want to be interfacing with this crate is through the handshake structs of
10//! which there are two: [handshake::ActiveHandshake] & [handshake::PassiveHandshake]. As the name
11//! suggests, the handshake is composed of an active and a passive part, usually being the server
12//! and the client respectively. The server is considered the active part of the handshake, because
13//! it will start the handshake procedure by sending the passive part (i.e. the client)
14//! initialization data. After which a few more messages will be exchanged until both parties have
15//! both setup their security bytes ([MessageCounter] & [Checksum]) as well as the shared encryption
16//! ([SilkroadEncryption]). Please refer to the documentation of the individual handshake element
17//! for more information about the procedure of a handshake.
18//!
19//! Each of the three elements ([MessageCounter], [Checksum], and [SilkroadEncryption]) can be used
20//! independently of each other if so desired.
21
22mod checksum;
23mod count;
24pub mod handshake;
25
26pub use crate::checksum::*;
27pub use crate::count::*;
28pub use crate::handshake::ActiveHandshake;
29pub use crate::handshake::PassiveHandshake;
30pub use crate::handshake::SecurityFeature;
31use blowfish::cipher::{Block, BlockDecrypt, BlockEncrypt, KeyInit};
32use blowfish::BlowfishLE;
33
34use bytes::{BufMut, Bytes};
35use thiserror::Error;
36
37const BLOCK_SIZE: usize = 8;
38// Convenience shorthand.
39type BlowfishBlock = Block<BlowfishLE>;
40
41#[derive(Error, Debug)]
42pub enum SilkroadSecurityError {
43 /// The handshake hasn't been started or hasn't completed, yet the operation required it.
44 #[error("Security has not been initialized")]
45 SecurityUninitialized,
46 /// The handshake has already completed. The security would need to be reset before continuing.
47 #[error("Security is already initialized")]
48 AlreadyInitialized,
49 /// Finalizing the handshake requires the handshake to have exchanged public key data, which hasn't happened yet.
50 #[error("Security has not completed the initialization")]
51 InitializationUnfinished,
52 /// The given encrypted data is not the correct block length, as required for decryption.
53 #[error("{0} is an invalid block length")]
54 InvalidBlockLength(usize),
55 /// We calculated a different secret than the client, something went wrong in the handshake.
56 #[error("Local calculated key was {calculated} but received {received}")]
57 KeyExchangeMismatch { received: u64, calculated: u64 },
58}
59
60const BLOWFISH_BLOCK_SIZE: usize = 8;
61
62/// Handles the encryption/decryption of data in Silkroad Online.
63///
64/// Generally only the client encrypts data, but this is a generic encryption setup. This is
65/// essentially a thin veil around [BlowfishCompat], only ensuring the right block size has been
66/// used.
67///
68/// You can create this with a predefined key:
69/// ```
70/// # use skrillax_security::SilkroadEncryption;
71/// let encryption = SilkroadEncryption::from_key(0xFF00FF00FF00FF00);
72/// ```
73/// Though generally the security should be set up through a handshake.
74pub struct SilkroadEncryption {
75 blowfish: Box<BlowfishLE>,
76}
77
78impl SilkroadEncryption {
79 /// Creates an encryption setup from a predefined key.
80 pub fn from_key(key: u64) -> Self {
81 Self {
82 blowfish: Box::new(blowfish_from_int(key)),
83 }
84 }
85
86 /// Decrypt an encrypted message sent by the client.
87 ///
88 /// Decrypts the given input by splitting it into the individual encrypted blocks. The output is all decrypted data,
89 /// which may contain padding that was added before encryption. Bytes are copied before performing decryption.
90 /// To decrypt in place, use [decrypt_mut][Self::decrypt_mut()].
91 ///
92 /// If the input doesn't match the required block length it will return [SilkroadSecurityError::InvalidBlockLength].
93 pub fn decrypt(&self, data: &[u8]) -> Result<Bytes, SilkroadSecurityError> {
94 let mut result = bytes::BytesMut::from(data);
95 self.decrypt_mut(&mut result)?;
96 Ok(result.freeze())
97 }
98
99 /// Decrypt an encrypted message sent by the client.
100 ///
101 /// Decrypts the given input by splitting it into the individual encrypted blocks in place. The decrypted data may
102 /// still be padded to match block length (8 bytes).
103 ///
104 /// If the input doesn't match the required block length it will return [SilkroadSecurityError::InvalidBlockLength].
105 pub fn decrypt_mut(&self, data: &mut [u8]) -> Result<(), SilkroadSecurityError> {
106 if data.len() % BLOWFISH_BLOCK_SIZE != 0 {
107 return Err(SilkroadSecurityError::InvalidBlockLength(data.len()));
108 }
109
110 for chunk in data.chunks_mut(BLOWFISH_BLOCK_SIZE) {
111 let block = BlowfishBlock::from_mut_slice(chunk);
112 self.blowfish.decrypt_block(block);
113 }
114 Ok(())
115 }
116
117 /// Encrypt a message to be sent to the client.
118 ///
119 /// Encrypts the given bytes using the previously established secret. Requires that the handshake has been completed.
120 /// It will copy the bytes and return the encrypted bytes as an owned reference. Bytes will be padded automatically
121 /// to the necessary block length. Use [encrypt_mut][Self::encrypt_mut()] for encryption in place.
122 pub fn encrypt(&self, data: &[u8]) -> Result<Bytes, SilkroadSecurityError> {
123 let target_buffer_size = Self::find_encrypted_length(data.len());
124 let mut result = bytes::BytesMut::with_capacity(target_buffer_size);
125 result.extend_from_slice(data);
126 for _ in 0..(target_buffer_size - data.len()) {
127 result.put_u8(0);
128 }
129 self.encrypt_mut(&mut result)?;
130 Ok(result.freeze())
131 }
132
133 /// Encrypt a message to be sent to the client.
134 ///
135 /// Encrypts the given bytes using the previously established secret. Requires that the handshake has been completed
136 /// and that `data` is a multiple of the block length.
137 ///
138 /// If the data is not block-aligned, will result in [SilkroadSecurityError::InvalidBlockLength]
139 pub fn encrypt_mut(&self, data: &mut [u8]) -> Result<(), SilkroadSecurityError> {
140 if data.len() % BLOCK_SIZE != 0 {
141 return Err(SilkroadSecurityError::InvalidBlockLength(data.len()));
142 }
143
144 for chunk in data.chunks_mut(BLOWFISH_BLOCK_SIZE) {
145 let block = BlowfishBlock::from_mut_slice(chunk);
146 self.blowfish.encrypt_block(block);
147 }
148 Ok(())
149 }
150
151 /// Find the nearest block-aligned length.
152 ///
153 /// Given the current length of data to encrypt, calculates the length of the encrypted output, which includes
154 /// padding. Can at most increase by `BLOWFISH_BLOCK_SIZE - 1`, which is `7`.
155 pub fn find_encrypted_length(given_length: usize) -> usize {
156 let aligned_length = given_length % BLOWFISH_BLOCK_SIZE;
157 if aligned_length == 0 {
158 // Already block-aligned, no need to pad
159 return given_length;
160 }
161
162 given_length + (8 - aligned_length) // Add padding
163 }
164}
165
166pub(crate) fn blowfish_from_int(key: u64) -> BlowfishLE {
167 BlowfishLE::new_from_slice(&key.to_le_bytes()).expect("Could not create blowfish key")
168}