skrillax_security/
handshake.rs

1//! The handshake module provides both sides of the handshake used to establish the security
2//! features of a connection between two Silkroad Online participants, usually a server and a
3//! client.
4//!
5//! The handshake always happens between an active and a passive party. The active party initiates
6//! the handshake and determines what features shall be used. The passive party essentially always
7//! accepts what is provided by the active party. This handshake how it is implemented here does not
8//! concern itself with actually exchanging the information. How that is done is up to the user of
9//! this api, and they might choose whatever makes sense in the given situation. You might also
10//! choose to use the [skrillax-stream](https://docs.rs/skrillax-stream) crate instead to handle the
11//! handshake for you in an async fashion.
12//!
13//! Generally, the active party is a server, while the passive party is a client. This at least
14//! holds true for the official Silkroad Online. Thus, if you want to interface or emulate it, you
15//! want to keep these roles. In any other situation, you may choose a different role assignment.
16//!
17//! The handshake of Silkroad Online is similar to a [Diffie–Hellman key exchange](https://en.wikipedia.org/wiki/Diffie%E2%80%93Hellman_key_exchange);
18//! Each party generates a private key (or something like a key). Then they perform an operation
19//! on the private key and share the resulting value. Using the shared value and performing the same
20//! operation using their original private key, they can generate a shared secret without having to
21//! ever share their private key. I say similar to, because the implementation Silkroad Online uses
22//! is a little weaker, as it's pretty easy to brute force, which I have shown in my [decryptor](https://github.com/kumpelblase2/skrillax/tree/master/silkroad-packet-decryptor#why-it-works).
23//! After the shared secret has been established, it is used as the key material for a blowfish
24//! cipher.
25//!
26//! Most of the internals are abstracted away in this implementation. If you're looking for a more
27//! in-depth overview of how the handshake works, you may want to look at the ['Silkroad Doc'](https://github.com/DummkopfOfHachtenduden/SilkroadDoc/wiki/silkroad-security).
28//!
29//! Depending on which party you're assuming, you either want to use the [ActiveHandshake], or
30//! [PassiveHandshake] if you're assumed to be the active party or passive party respectively.
31
32use crate::{blowfish_from_int, BlowfishBlock, SilkroadEncryption, SilkroadSecurityError};
33use bitflags::bitflags;
34use blowfish::cipher::{BlockDecrypt, BlockEncrypt};
35use blowfish::BlowfishLE;
36use byteorder::{ByteOrder, LittleEndian};
37use rand::random;
38
39bitflags! {
40    /// Defines the available security features of a Silkroad Online connection.
41    pub struct SecurityFeature: u8 {
42        /// Adds the checks/Generates security bytes (CRC & Count) for all packets.
43        const CHECKS = 1;
44        /// Allows/Enables the encryption of packets.
45        const ENCRYPTION = 2;
46    }
47}
48
49impl Default for SecurityFeature {
50    fn default() -> Self {
51        SecurityFeature::all()
52    }
53}
54
55#[derive(Copy, Clone)]
56struct ActiveEncryptionData {
57    handshake_seed: u64,
58    value_x: u32,
59    value_p: u32,
60    value_a: u32,
61}
62
63#[derive(Default)]
64enum ActiveHandshakeState {
65    #[default]
66    Uninitialized,
67    HandshakeStarted {
68        encryption_seed: Option<ActiveEncryptionData>,
69    },
70    Challenged {
71        blowfish: Box<BlowfishLE>,
72    },
73    FinishedEmpty,
74}
75
76#[derive(Copy, Clone)]
77pub struct PassiveEncryptionInitializationData {
78    pub seed: u64,
79    pub handshake_seed: u64,
80    pub additional_seeds: [u32; 3],
81}
82
83#[derive(Copy, Clone)]
84pub struct CheckBytesInitialization {
85    pub count_seed: u32,
86    pub crc_seed: u32,
87}
88
89#[derive(Copy, Clone)]
90pub struct PassiveInitializationData {
91    pub checks: Option<CheckBytesInitialization>,
92    pub encryption_seed: Option<PassiveEncryptionInitializationData>,
93}
94
95/// Provides the active part of a handshake.
96///
97/// The active part of the handshake initializes the handshake procedure. It will generate the
98/// necessary initialization data and provide the passive part with a challenge, before it will
99/// complete the handshake. Generally, you're expected to transfer the returned data to the other
100/// side by whatever means appropriate. Depending on the security features that you want, you can
101/// decide to end the handshake early. However, the following will assume you want all security
102/// features enabled.
103///
104/// An example procedure would be as follows:
105/// ```
106/// # use skrillax_security::{ActiveHandshake, PassiveHandshake, SecurityFeature};
107/// let mut handshake = ActiveHandshake::default();
108/// let init = handshake.initialize(SecurityFeature::all()).expect("Should be able to initialize handshake.");
109/// # let mut passive = PassiveHandshake::default();
110/// // You should now transfer the data contained in `init` to the other side.
111/// // The other side would then give you their public part of their side. With that, you can
112/// // generate a challenge.
113/// # let (value_key, value_b) = passive.initialize(init.encryption_seed).unwrap().unwrap();
114/// // You should get `value_b` & `value_key` from the passive side of the handshake.
115/// let challenge = handshake.start_challenge(value_b, value_key).expect("Should be able to start the challenge.");
116/// // This challenge should again be transferred to the other side. At this point the handshake it
117/// // technically complete; all data has been exchanged. However, we should wait for the passive
118/// // side to acknowledge the challenge.
119/// # passive.finish(challenge).unwrap();
120/// let encryption = handshake.finish().expect("Should have finished handshake.").expect("Encryption should've been established.");
121/// ```
122#[derive(Default)]
123pub struct ActiveHandshake {
124    features: SecurityFeature,
125    state: ActiveHandshakeState,
126}
127
128impl ActiveHandshake {
129    /// Starts the handshake process.
130    ///
131    /// This generates the private key parts and returns [PassiveInitializationData], which
132    /// should be transferred to the client. This should later be followed by calling
133    /// [start_challenge()][Self::start_challenge()] with the client response. The content of the
134    /// [PassiveInitializationData] may vary depending on the configured security features.
135    ///
136    /// If a handshake has already been started or completed, will return [SilkroadSecurityError::AlreadyInitialized].
137    pub fn initialize(
138        &mut self,
139        features: SecurityFeature,
140    ) -> Result<PassiveInitializationData, SilkroadSecurityError> {
141        if !matches!(self.state, ActiveHandshakeState::Uninitialized) {
142            return Err(SilkroadSecurityError::AlreadyInitialized);
143        }
144
145        let check_init = if features.contains(SecurityFeature::CHECKS) {
146            Some((u32::from(random::<u8>()), u32::from(random::<u8>())))
147        } else {
148            None
149        };
150
151        if features.contains(SecurityFeature::ENCRYPTION) {
152            let seed = random::<u64>();
153            let handshake_seed = random::<u64>();
154            let value_x = random::<u32>() & 0x7FFFFFFF;
155            let value_g = random::<u32>() & 0x7FFFFFFF;
156            let value_p = random::<u32>() & 0x7FFFFFFF;
157            let value_a = g_pow_x_mod_p(value_p.into(), value_x, value_g);
158            self.state = ActiveHandshakeState::HandshakeStarted {
159                encryption_seed: Some(ActiveEncryptionData {
160                    handshake_seed,
161                    value_x,
162                    value_p,
163                    value_a,
164                }),
165            };
166
167            Ok(PassiveInitializationData {
168                checks: check_init.map(|(crc, count)| CheckBytesInitialization {
169                    count_seed: count,
170                    crc_seed: crc,
171                }),
172                encryption_seed: Some(PassiveEncryptionInitializationData {
173                    seed,
174                    handshake_seed,
175                    additional_seeds: [value_g, value_p, value_a],
176                }),
177            })
178        } else {
179            self.state = ActiveHandshakeState::FinishedEmpty;
180            Ok(PassiveInitializationData {
181                checks: check_init.map(|(crc, count)| CheckBytesInitialization {
182                    count_seed: count,
183                    crc_seed: crc,
184                }),
185                encryption_seed: None,
186            })
187        }
188    }
189
190    /// Initialize the security with a predefined set of values.
191    /// These are the same values that would be generated randomly in [initialize()].
192    /// This effectively does the initialization, just with the predefined values,
193    /// resulting in a deterministic handshake.
194    #[allow(unused)]
195    fn initialize_with(&mut self, encryption_data: Option<ActiveEncryptionData>) {
196        self.state = ActiveHandshakeState::HandshakeStarted {
197            encryption_seed: encryption_data,
198        }
199    }
200
201    /// Create a challenge to the client.
202    ///
203    /// This creates a challenge for the client, signaling a switch to an encrypted channel using the exchanged key
204    /// material. We also check if the key, that the client sent us, matches what we would expect given what we've
205    /// witnessed in the key exchange.
206    ///
207    /// If successful, returns the challenge for the client. If [initialize][Self::initialize()] hasn't been called,
208    /// returns [SilkroadSecurityError::SecurityUninitialized]. If the passed key does not match the key we expected,
209    /// will return [SilkroadSecurityError::KeyExchangeMismatch].
210    pub fn start_challenge(
211        &mut self,
212        value_b: u32,
213        client_key: u64,
214    ) -> Result<u64, SilkroadSecurityError> {
215        let ActiveHandshakeState::HandshakeStarted { encryption_seed } = self.state else {
216            return Err(SilkroadSecurityError::SecurityUninitialized);
217        };
218
219        let Some(encryption_setup) = encryption_seed else {
220            return Err(SilkroadSecurityError::SecurityUninitialized);
221        };
222
223        let value_k = g_pow_x_mod_p(
224            encryption_setup.value_p.into(),
225            encryption_setup.value_x,
226            value_b,
227        );
228        let new_key = to_u64(encryption_setup.value_a, value_b);
229        let new_key = transform_key(new_key, value_k, LOBYTE(LOWORD(value_k)) & 0x03);
230        let blowfish = blowfish_from_int(new_key);
231
232        let mut key_bytes: [u8; 8] = client_key.to_le_bytes();
233        blowfish.decrypt_block(BlowfishBlock::from_mut_slice(&mut key_bytes));
234
235        let client_key = LittleEndian::read_u64(&key_bytes);
236        let new_key = to_u64(value_b, encryption_setup.value_a);
237        let new_key = transform_key(new_key, value_k, LOBYTE(LOWORD(value_b)) & 0x07);
238        if new_key != client_key {
239            return Err(SilkroadSecurityError::KeyExchangeMismatch {
240                received: client_key,
241                calculated: new_key,
242            });
243        }
244
245        let new_key = to_u64(encryption_setup.value_a, value_b);
246        let new_key = transform_key(new_key, value_k, LOBYTE(LOWORD(value_k)) & 0x03);
247        let blowfish = blowfish_from_int(new_key);
248
249        let challenge_key = to_u64(encryption_setup.value_a, value_b);
250        let challenge_key = transform_key(
251            challenge_key,
252            value_k,
253            LOBYTE(LOWORD(encryption_setup.value_a)) & 0x07,
254        );
255        let mut key_bytes: [u8; 8] = challenge_key.to_le_bytes();
256        blowfish.encrypt_block(BlowfishBlock::from_mut_slice(&mut key_bytes));
257        let encrypted_challenge = LittleEndian::read_u64(&key_bytes);
258
259        let handshake_seed = transform_key(encryption_setup.handshake_seed, value_k, 3);
260
261        self.state = ActiveHandshakeState::Challenged {
262            blowfish: Box::new(blowfish_from_int(handshake_seed)),
263        };
264
265        Ok(encrypted_challenge)
266    }
267
268    /// Finishes the handshake process.
269    ///
270    /// This will try to finish the handshake process, at whatever stage we are. Depending on the
271    /// configured settings, this may be at different stages.
272    /// If no security features are configured: at any point.
273    /// If only check bytes are configured: after initialization.
274    /// If encryption is configured: after having created the client challenge.
275    pub fn finish(self) -> Result<Option<SilkroadEncryption>, SilkroadSecurityError> {
276        match self.state {
277            ActiveHandshakeState::Challenged { blowfish } => {
278                Ok(Some(SilkroadEncryption { blowfish }))
279            }
280            ActiveHandshakeState::Uninitialized if self.features.is_empty() => Ok(None),
281            ActiveHandshakeState::FinishedEmpty => Ok(None),
282            ActiveHandshakeState::HandshakeStarted { encryption_seed }
283                if encryption_seed.is_none() =>
284            {
285                Ok(None)
286            }
287            _ => Err(SilkroadSecurityError::InitializationUnfinished),
288        }
289    }
290}
291
292struct PassiveEncryptionData {
293    blowfish: Box<BlowfishLE>,
294    local_public: u32,
295    remote_public: u32,
296    shared_secret: u32,
297    initial_seed: u64,
298}
299
300#[derive(Default)]
301enum PassiveHandshakeState {
302    #[default]
303    Uninitialized,
304    AuthStarted {
305        encryption_seed: Option<PassiveEncryptionData>,
306    },
307    Challenging {
308        blowfish: Box<BlowfishLE>,
309    },
310}
311
312/// Provides the passive part of the handshake.
313///
314/// The passive part of the handshake only really responds to the stuff the active part tells it.
315/// This includes the enabled features, which cannot be configured here but will be part of the
316/// setup sent from the active part. We're currently expecting the other side to initialize a
317/// handshake, but this is technically not required. How you deal with that is up to you.
318///
319/// An example exchange could look like this:
320/// ```
321/// # use skrillax_security::{ActiveHandshake, PassiveHandshake, SecurityFeature};
322/// # let mut handshake = ActiveHandshake::default();
323/// # let init = handshake.initialize(SecurityFeature::all()).expect("Should be able to initialize handshake.");
324/// let mut passive = PassiveHandshake::default();
325/// // The active side will create an initialization, which will be sent to the passive side.
326/// // We simply plug that information into our procedure. For now, we assume they're setting up
327/// // encryption as well, so we simply `unwrap()` everything here.
328/// let (value_key, value_b) = passive.initialize(init.encryption_seed).unwrap().unwrap();
329/// // These two values should then be sent to the active side again. That side will then respond
330/// // with a challenge for us, to verify everything went fine.
331/// # let challenge = handshake.start_challenge(value_b, value_key).expect("Should be able to start the challenge.");
332/// passive.finish(challenge).unwrap();
333/// // We then need to complete the handshake, which we should signal to the active part as well.
334/// // `finish` and `done` are separate things, because if the active part does not actually send
335/// // encryption information we can't call `finish`. We'd instead just call `done`.
336/// let encryption = passive.done().expect("Handshake should have completed.");
337/// ```
338#[derive(Default)]
339pub struct PassiveHandshake {
340    state: PassiveHandshakeState,
341}
342
343impl PassiveHandshake {
344    /// Initialize the handshake with the data from the active side.
345    ///
346    /// We have received the initialization data from the active handshake side and want to
347    /// initialize our side as well. Depending on the security features selected by the active side,
348    /// the initialization data may actually be `None`, which is why this accepts and [Option].
349    /// Technically, if you haven't received any encryption initialization data, you can simply call
350    /// [PassiveHandshake::done] and complete the handshake - there's nothing more to be exchanged.
351    /// This is essentially a convenience to stay more consistent with what we receive from the
352    /// active part.
353    ///
354    /// This may error if we're already initialized, returning [SilkroadSecurityError::InitializationUnfinished].
355    pub fn initialize(
356        &mut self,
357        init: Option<PassiveEncryptionInitializationData>,
358    ) -> Result<Option<(u64, u32)>, SilkroadSecurityError> {
359        if !matches!(self.state, PassiveHandshakeState::Uninitialized) {
360            return Err(SilkroadSecurityError::InitializationUnfinished);
361        }
362
363        let (encryption_data, challenge) = if let Some(encryption_setup) = &init {
364            let value_g = encryption_setup.additional_seeds[0];
365            let value_p = encryption_setup.additional_seeds[1];
366            let value_a = encryption_setup.additional_seeds[2];
367            let local_private = random::<u32>();
368            let remote_public = g_pow_x_mod_p(value_p as i64, local_private, value_g);
369            let shared_secret = g_pow_x_mod_p(value_p as i64, local_private, value_a);
370            let key = transform_key(
371                to_u64(value_a, remote_public),
372                shared_secret,
373                LOBYTE(LOWORD(shared_secret)) & 0x03,
374            );
375            let blowfish = blowfish_from_int(key);
376            let challenge = transform_key(
377                to_u64(remote_public, value_a),
378                shared_secret,
379                LOBYTE(LOWORD(remote_public)) & 0x07,
380            );
381            let mut challenge_bytes: [u8; 8] = challenge.to_le_bytes();
382            blowfish.encrypt_block(BlowfishBlock::from_mut_slice(&mut challenge_bytes));
383            let encrypted_challenge = u64::from_le_bytes(challenge_bytes);
384            (
385                Some(PassiveEncryptionData {
386                    blowfish: Box::new(blowfish),
387                    local_public: value_a,
388                    remote_public,
389                    shared_secret,
390                    initial_seed: encryption_setup.handshake_seed,
391                }),
392                Some((encrypted_challenge, remote_public)),
393            )
394        } else {
395            (None, None)
396        };
397
398        self.state = PassiveHandshakeState::AuthStarted {
399            encryption_seed: encryption_data,
400        };
401
402        Ok(challenge)
403    }
404
405    /// Complete the handshake by verifying the challenge.
406    ///
407    /// After we have sent our initialization data to the active part, they provide us with a sort
408    /// of challenge. If we can verify the challenge with what we internally calculated, we know the
409    /// key exchange was successful, and we now have a shared secret. At this point, the handshake
410    /// is essentially completed. This should be signaled to the active side by switching to an
411    /// encrypted channel.
412    pub fn finish(&mut self, challenge: u64) -> Result<(), SilkroadSecurityError> {
413        let PassiveHandshakeState::AuthStarted {
414            encryption_seed: Some(ref encryption_data),
415        } = self.state
416        else {
417            return Err(SilkroadSecurityError::InitializationUnfinished);
418        };
419
420        let expected = to_u64(encryption_data.local_public, encryption_data.remote_public);
421        let expected_key = transform_key(
422            expected,
423            encryption_data.shared_secret,
424            LOBYTE(LOWORD(encryption_data.local_public)) & 0x07,
425        );
426        let mut expected_key_bytes: [u8; 8] = expected_key.to_le_bytes();
427        encryption_data
428            .blowfish
429            .encrypt_block(BlowfishBlock::from_mut_slice(&mut expected_key_bytes));
430        let encrypted_key = u64::from_le_bytes(expected_key_bytes);
431        if encrypted_key != challenge {
432            return Err(SilkroadSecurityError::KeyExchangeMismatch {
433                received: challenge,
434                calculated: encrypted_key,
435            });
436        }
437        let transformed_key = transform_key(
438            encryption_data.initial_seed,
439            encryption_data.shared_secret,
440            3,
441        );
442        let blowfish = Box::new(blowfish_from_int(transformed_key));
443
444        self.state = PassiveHandshakeState::Challenging { blowfish };
445
446        Ok(())
447    }
448
449    /// Return the resulting encryption from the handshake.
450    ///
451    /// If the selected security features of the active handshake part included setting up the
452    /// encryption, the final result will be returned. If it didn't contain that feature, it will
453    /// return `None` instead.
454    ///
455    /// Will return [SilkroadSecurityError::InitializationUnfinished] if we haven't completed the
456    /// handshake yet.
457    pub fn done(self) -> Result<Option<SilkroadEncryption>, SilkroadSecurityError> {
458        match self.state {
459            PassiveHandshakeState::AuthStarted { encryption_seed } if encryption_seed.is_some() => {
460                Err(SilkroadSecurityError::InitializationUnfinished)
461            }
462            PassiveHandshakeState::Challenging { blowfish } => {
463                Ok(Some(SilkroadEncryption { blowfish }))
464            }
465            _ => Ok(None),
466        }
467    }
468}
469
470#[allow(unused_parens)]
471fn transform_key(val: u64, key: u32, key_byte: u8) -> u64 {
472    let mut stream = val.to_le_bytes();
473
474    stream[0] ^= (stream[0]
475        .wrapping_add(LOBYTE(LOWORD(key)))
476        .wrapping_add(key_byte));
477    stream[1] ^= (stream[1]
478        .wrapping_add(HIBYTE(LOWORD(key)))
479        .wrapping_add(key_byte));
480    stream[2] ^= (stream[2]
481        .wrapping_add(LOBYTE(HIWORD(key)))
482        .wrapping_add(key_byte));
483    stream[3] ^= (stream[3]
484        .wrapping_add(HIBYTE(HIWORD(key)))
485        .wrapping_add(key_byte));
486    stream[4] ^= (stream[4]
487        .wrapping_add(LOBYTE(LOWORD(key)))
488        .wrapping_add(key_byte));
489    stream[5] ^= (stream[5]
490        .wrapping_add(HIBYTE(LOWORD(key)))
491        .wrapping_add(key_byte));
492    stream[6] ^= (stream[6]
493        .wrapping_add(LOBYTE(HIWORD(key)))
494        .wrapping_add(key_byte));
495    stream[7] ^= (stream[7]
496        .wrapping_add(HIBYTE(HIWORD(key)))
497        .wrapping_add(key_byte));
498
499    LittleEndian::read_u64(&stream)
500}
501
502#[allow(non_snake_case)]
503fn LOWORD(a: u32) -> u16 {
504    (a & 0xFFFF) as u16
505}
506
507#[allow(non_snake_case)]
508fn HIWORD(a: u32) -> u16 {
509    ((a >> 16) & 0xFFFF) as u16
510}
511
512#[allow(non_snake_case)]
513fn LOBYTE(a: u16) -> u8 {
514    (a & 0xFF) as u8
515}
516
517#[allow(non_snake_case)]
518fn HIBYTE(a: u16) -> u8 {
519    ((a >> 8) & 0xFF) as u8
520}
521
522fn g_pow_x_mod_p(p: i64, mut x: u32, g: u32) -> u32 {
523    let mut current: i64 = 1;
524    let mut mult: i64 = g as i64;
525
526    while x != 0 {
527        if (x & 1) > 0 {
528            current = (mult * current) % p;
529        }
530        x >>= 1;
531        mult = (mult * mult) % p;
532    }
533    current as u32
534}
535
536fn to_u64(low: u32, high: u32) -> u64 {
537    ((high as u64) << 32) | low as u64
538}
539
540#[cfg(test)]
541mod test {
542    use super::*;
543
544    #[test]
545    fn test_equal() {
546        let mut server_handshake = ActiveHandshake::default();
547        let mut client_handshake = PassiveHandshake::default();
548
549        let init = server_handshake
550            .initialize(SecurityFeature::all())
551            .expect("should be able to initialize");
552        assert!(init.encryption_seed.is_some());
553        assert!(init.checks.is_some());
554        let (key, value_b) = client_handshake
555            .initialize(init.encryption_seed)
556            .expect("should accept initialization")
557            .unwrap();
558        let response = server_handshake
559            .start_challenge(value_b, key)
560            .expect("should accept challenge");
561        client_handshake
562            .finish(response)
563            .expect("should do challenge");
564        let active_encryption = server_handshake
565            .finish()
566            .expect("server should be finished.")
567            .unwrap();
568        let passive_encryption = client_handshake
569            .done()
570            .expect("client should be finished.")
571            .unwrap();
572
573        let encrypted = active_encryption
574            .encrypt(&[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08])
575            .expect("Should be able to encrypt");
576
577        let decrypted = passive_encryption
578            .decrypt(&encrypted)
579            .expect("Should be able to decrypt");
580
581        assert_eq!(
582            &[0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08],
583            decrypted.as_ref()
584        );
585    }
586
587    #[test]
588    fn finishes_encoding() {
589        let handshake_seed =
590            LittleEndian::read_u64(&[0xbf, 0x89, 0x96, 0x76, 0xae, 0x97, 0x5e, 0x17]);
591        let _value_g = LittleEndian::read_u32(&[0x95, 0x0b, 0xf5, 0x20]);
592        let value_p = LittleEndian::read_u32(&[0x0d, 0xf4, 0x13, 0x52]);
593        let value_x = 189993144; // brute forced
594        let value_a = LittleEndian::read_u32(&[0x36, 0x44, 0x96, 0x24]);
595
596        let mut security = ActiveHandshake::default();
597        security.initialize_with(Some(ActiveEncryptionData {
598            handshake_seed,
599            value_x,
600            value_p,
601            value_a,
602        }));
603
604        let value_b = LittleEndian::read_u32(&[0x7a, 0x04, 0x39, 0x43]);
605        let key = LittleEndian::read_u64(&[0x69, 0x02, 0xec, 0x3f, 0x16, 0xbb, 0x18, 0x64]);
606
607        let result = security.start_challenge(value_b, key).unwrap();
608
609        let result_expected_bytes = &[0xbe, 0x6f, 0x5e, 0xd4, 0x19, 0x79, 0x7d, 0x26];
610        let result_expected = LittleEndian::read_u64(result_expected_bytes);
611
612        assert_eq!(result, result_expected);
613        assert!(security.finish().is_ok());
614    }
615}