Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions crates/buttplug_server/src/device/protocol_impl/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ pub mod tryfun;
pub mod tryfun_blackhole;
pub mod tryfun_meta2;
pub mod vibcrafter;
pub mod vibio;
pub mod vibratissimo;
pub mod vorze_sa;
pub mod wetoy;
Expand Down Expand Up @@ -540,6 +541,10 @@ pub fn get_default_protocol_map() -> HashMap<String, Arc<dyn ProtocolIdentifierF
&mut map,
vibcrafter::setup::VibCrafterIdentifierFactory::default(),
);
add_to_protocol_map(
&mut map,
vibio::setup::VibioIdentifierFactory::default(),
);
add_to_protocol_map(
&mut map,
vibratissimo::setup::VibratissimoIdentifierFactory::default(),
Expand Down
173 changes: 173 additions & 0 deletions crates/buttplug_server/src/device/protocol_impl/vibio.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// Buttplug Rust Source Code File - See https://buttplug.io for more info.
//
// Copyright 2016-2026 Nonpolynomial Labs LLC. All rights reserved.
//
// Licensed under the BSD 3-Clause license. See LICENSE file in the project root
// for full license information.

use crate::device::{
hardware::{Hardware, HardwareCommand, HardwareEvent, HardwareSubscribeCmd, HardwareWriteCmd},
protocol::{
ProtocolHandler,
ProtocolIdentifier,
ProtocolInitializer,
generic_protocol_initializer_setup,
},
};
use aes::Aes128;
use async_trait::async_trait;
use buttplug_core::errors::ButtplugDeviceError;
use buttplug_server_device_config::Endpoint;
use buttplug_server_device_config::{
ProtocolCommunicationSpecifier,
ServerDeviceDefinition,
UserDeviceIdentifier,
};
use ecb::cipher::block_padding::Pkcs7;
use ecb::cipher::{BlockDecryptMut, BlockEncryptMut, KeyInit};
use std::sync::{
Arc,
atomic::{AtomicU8, Ordering},
};
use uuid::{Uuid, uuid};

use rand::distr::Alphanumeric;
use rand::RngExt;
use regex::Regex;
use sha2::{Digest, Sha256};

type Aes128EcbEnc = ecb::Encryptor<Aes128>;
type Aes128EcbDec = ecb::Decryptor<Aes128>;

const VIBIO_PROTOCOL_UUID: Uuid = uuid!("b8c76c9e-cb42-4a94-99f4-7c2a8e5d3b2a");
const VIBIO_KEY: [u8; 16] = *b"jdk#vib%y5fir21a";

generic_protocol_initializer_setup!(Vibio, "vibio");

#[derive(Default)]
pub struct VibioInitializer {}

fn encrypt(command: String) -> Vec<u8> {
let enc = Aes128EcbEnc::new(&VIBIO_KEY.into());
let res = enc.encrypt_padded_vec_mut::<Pkcs7>(command.as_bytes());

info!("Encoded {} to {:?}", command, res);
res
}

fn decrypt(data: Vec<u8>) -> String {
let dec = Aes128EcbDec::new(&VIBIO_KEY.into());
let res = String::from_utf8(dec.decrypt_padded_vec_mut::<Pkcs7>(&data).unwrap()).unwrap();

info!("Decoded {} from {:?}", res, data);
res
}

#[async_trait]
impl ProtocolInitializer for VibioInitializer {
async fn initialize(
&mut self,
hardware: Arc<Hardware>,
_: &ServerDeviceDefinition,
) -> Result<Arc<dyn ProtocolHandler>, ButtplugDeviceError> {
let mut event_receiver = hardware.event_stream();
hardware
.subscribe(&HardwareSubscribeCmd::new(
VIBIO_PROTOCOL_UUID,
Endpoint::Rx,
))
.await?;

let auth_str = rand::rng()
.sample_iter(&Alphanumeric)
.take(8)
.map(char::from)
.collect::<String>();
let auth_msg = format!("Auth:{};", auth_str);
hardware
.write_value(&HardwareWriteCmd::new(
&[VIBIO_PROTOCOL_UUID],
Endpoint::Tx,
encrypt(auth_msg),
false,
))
.await?;

loop {
let event = event_receiver.recv().await;
if let Ok(HardwareEvent::Notification(_, _, n)) = event {
let decoded = decrypt(n);
if decoded.eq("OK;") {
debug!("Vibio authenticated!");
return Ok(Arc::new(Vibio::default()));
}
let challenge = Regex::new(r"^([0-9A-Fa-f]{4}):([^;]+);$")
.expect("This is static and should always compile");
if let Some(parts) = challenge.captures(decoded.as_str()) {
debug!("Vibio challenge {:?}", parts);
if let Some(to_hash) = parts.get(2) {
debug!("Vibio to hash {:?}", to_hash);
let mut sha256 = Sha256::new();
sha256.update(to_hash.as_str().as_bytes());
let result = &sha256.finalize();

let auth_msg = format!("Auth:{:02x}{:02x};", result[0], result[1]);
hardware
.write_value(&HardwareWriteCmd::new(
&[VIBIO_PROTOCOL_UUID],
Endpoint::Tx,
encrypt(auth_msg),
false,
))
.await?;
} else {
return Err(ButtplugDeviceError::ProtocolSpecificError(
"Vibio".to_owned(),
"Vibio didn't provide a valid security handshake".to_owned(),
));
}
} else {
return Err(ButtplugDeviceError::ProtocolSpecificError(
"Vibio".to_owned(),
"Vibio didn't provide a valid security handshake".to_owned(),
));
}
} else {
return Err(ButtplugDeviceError::ProtocolSpecificError(
"Vibio".to_owned(),
"Vibio didn't provide a valid security handshake".to_owned(),
));
}
}
}
}

#[derive(Default)]
pub struct Vibio {
speeds: [AtomicU8; 2],
}

impl ProtocolHandler for Vibio {
fn handle_output_vibrate_cmd(
&self,
feature_index: u32,
feature_id: uuid::Uuid,
speed: u32,
) -> Result<Vec<HardwareCommand>, ButtplugDeviceError> {
self.speeds[feature_index as usize].store(speed as u8, Ordering::Relaxed);

Ok(vec![
HardwareWriteCmd::new(
&[feature_id],
Endpoint::Tx,
encrypt(format!(
"MtInt:{:02}{:02};",
self.speeds[0].load(Ordering::Relaxed),
self.speeds[1].load(Ordering::Relaxed)
)),
false,
)
.into(),
])
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"version": {
"major": 4,
"minor": 179
"minor": 180
},
"protocols": {
"activejoy": {
Expand Down Expand Up @@ -21773,6 +21773,140 @@
"name": "VibCrafter Device"
}
},
"vibio": {
"communication": [
{
"btle": {
"names": [
"Clara_Vibio",
"Dodson_Vibio",
"Elle_Vibio",
"Frida_Vibio",
"Rivera_Vibio"
],
"services": {
"53300021-0050-4bd4-bbe5-a6920e4c5663": {
"rx": "53300023-0050-4bd4-bbe5-a6920e4c5663",
"tx": "53300022-0050-4bd4-bbe5-a6920e4c5663"
}
}
}
}
],
"configurations": [
{
"features": [
{
"id": "343a8e18-b76c-4482-b048-32d762bf87c9",
"index": 0,
"output": {
"vibrate": {
"value": [
0,
99
]
}
}
}
],
"id": "b55fef0e-baa3-44d0-9545-a4b7b0298515",
"identifier": [
"Clara_Vibio"
],
"name": "Vibio Clara"
},
{
"features": [
{
"id": "343a8e18-b76c-4482-b048-32d762bf87c9",
"index": 0,
"output": {
"vibrate": {
"value": [
0,
99
]
}
}
}
],
"id": "c66fef0e-cbb4-44d0-9545-a4b7b0298516",
"identifier": [
"Dodson_Vibio"
],
"name": "Vibio Dodson"
},
{
"id": "d77fef0e-dcc5-44d0-9545-a4b7b0298517",
"identifier": [
"Rivera_Vibio"
],
"name": "Vibio Rivera"
},
{
"id": "e88fef0e-edd6-44d0-9545-a4b7b0298518",
"identifier": [
"Elle_Vibio"
],
"name": "Vibio Elle"
},
{
"id": "f99fef0e-fee7-44d0-9545-a4b7b0298519",
"identifier": [
"Frida_Vibio"
],
"name": "Vibio Frida"
}
],
"defaults": {
"features": [
{
"id": "343a8e18-b76c-4482-b048-32d762bf87c9",
"index": 0,
"output": {
"vibrate": {
"value": [
0,
99
]
}
}
},
{
"id": "d92a031e-bd0d-4815-a0bd-6c59566dcce2",
"index": 1,
"output": {
"vibrate": {
"value": [
0,
99
]
}
}
},
{
"description": "Battery Level",
"id": "e1a2b3c4-d5e6-f7a0-b1c2-d3e4f5a6b7c8",
"index": 2,
"input": {
"battery": {
"command": [
"Read"
],
"value": [
[
0,
100
]
]
}
}
}
],
"id": "a44eef0e-b412-44d0-9545-a4b7b0298514",
"name": "Vibio Device"
}
},
"vibratissimo": {
"communication": [
{
Expand Down
Loading