1 //! The migrate module is intended to make it possible to migrate device
2 //! information and settings between BlueZ and Floss.
3 //!
4 //! The rules for [source] -> [target] migration:
5 //! - All devices that exist in [source] must exist in [target]. Delete
6 //! ones that don't exist in [source].
7 //! - If the device exists in both [source] and [target], replace [target]
8 //! keys with transferred [source] keys, but keep all keys that don't
9 //! exist in [source]
10 //! - Drop devices that run into issues, but continue trying to migrate
11 //! all others
12
13 use std::collections::HashMap;
14 use std::fmt::Write;
15 use std::fs;
16 use std::path::Path;
17
18 use configparser::ini::Ini;
19 use glob::glob;
20
21 use log::{debug, error, info, warn};
22
23 const BT_LIBDIR: &str = "/var/lib/bluetooth";
24 pub const FLOSS_CONF_FILE: &str = "/var/lib/bluetooth/bt_config.conf";
25
26 const ADAPTER_SECTION_NAME: &str = "Adapter";
27 const GENERAL_SECTION_NAME: &str = "General";
28 const LINKKEY_SECTION_NAME: &str = "LinkKey";
29 const DEVICEID_SECTION_NAME: &str = "DeviceID";
30 const IRK_SECTION_NAME: &str = "IdentityResolvingKey";
31 const LTK_SECTION_NAME: &str = "LongTermKey";
32 const BLUEZ_PERIPHERAL_LTK_SECTION_NAME: &str = "PeripheralLongTermKey";
33 const BLUEZ_LOCAL_LTK_SECTION_NAME: &str = "SlaveLongTermKey";
34 const REPORT_MAP_SECTION_NAME: &str = "ReportMap";
35
36 const CLASSIC_TYPE: &str = "BR/EDR;";
37 const LE_TYPE: &str = "LE;";
38 const DUAL_TYPE: &str = "BR/EDR;LE;";
39
40 /// Represents LTK info
41 #[derive(Debug, Default)]
42 struct LtkInfo {
43 key: u128,
44 rand: u64,
45 ediv: u16,
46 auth: u8,
47 len: u8,
48 }
49
50 impl LtkInfo {
51 // BlueZ has 5 valid possibilities of auth (b/329392926).
52 // For simplicity, we only map it to the 2 values Floss supported.
53 // This way we can't distinguish whether the device is using legacy or secure pairing.
auth_from_bluez(bluez_auth: u8) -> u854 fn auth_from_bluez(bluez_auth: u8) -> u8 {
55 match bluez_auth {
56 0 | 2 | 4 => 1, // unauthenticated
57 1 | 3 => 2, // authenticated
58 _ => 0, // invalid
59 }
60 }
61
auth_to_bluez(floss_auth: u8) -> u862 fn auth_to_bluez(floss_auth: u8) -> u8 {
63 match floss_auth {
64 1 => 2, // unauthenticated, secure pairing
65 2 => 3, // authenticated, secure pairing
66 _ => 5, // invalid
67 }
68 }
69
new_from_bluez(bluez_conf: &Ini, sec: &str) -> Self70 fn new_from_bluez(bluez_conf: &Ini, sec: &str) -> Self {
71 LtkInfo {
72 key: u128::from_str_radix(bluez_conf.get(sec, "Key").unwrap_or_default().as_str(), 16)
73 .unwrap_or_default(),
74 rand: bluez_conf
75 .get(sec, "Rand")
76 .unwrap_or_default()
77 .parse::<u64>()
78 .unwrap_or_default(),
79 ediv: bluez_conf
80 .get(sec, "EDiv")
81 .unwrap_or_default()
82 .parse::<u16>()
83 .unwrap_or_default(),
84 auth: LtkInfo::auth_from_bluez(
85 bluez_conf
86 .get(sec, "Authenticated")
87 .unwrap_or_default()
88 .parse::<u8>()
89 .unwrap_or_default(),
90 ),
91 len: bluez_conf
92 .get(sec, "EncSize")
93 .unwrap_or_default()
94 .parse::<u8>()
95 .unwrap_or_default(),
96 }
97 }
98
99 // LE_KEY_PENC = LTK + RAND (64) + EDIV (16) + Security Level (8) + Key Length (8)
try_from_penc(val: String) -> Result<Self, &'static str>100 fn try_from_penc(val: String) -> Result<Self, &'static str> {
101 if val.len() != 56 {
102 return Err("PENC String provided to LtkInfo is not the right size");
103 }
104
105 Ok(LtkInfo {
106 key: u128::from_str_radix(&val[0..32], 16).unwrap_or_default(),
107 rand: u64::from_str_radix(&val[32..48], 16).unwrap_or_default().swap_bytes(),
108 ediv: u16::from_str_radix(&val[48..52], 16).unwrap_or_default().swap_bytes(),
109 auth: u8::from_str_radix(&val[52..54], 16).unwrap_or_default(),
110 len: u8::from_str_radix(&val[54..56], 16).unwrap_or_default(),
111 })
112 }
113
try_into_penc(self) -> Result<String, &'static str>114 fn try_into_penc(self) -> Result<String, &'static str> {
115 Ok(format!(
116 "{:032x}{:016x}{:04x}{:02x}{:02x}",
117 self.key,
118 self.rand.swap_bytes(),
119 self.ediv.swap_bytes(),
120 self.auth,
121 self.len
122 ))
123 }
124
125 // LE_KEY_LENC = LTK + EDIV (16) + Key Size (8) + Security Level (8)
try_from_lenc(val: String) -> Result<Self, &'static str>126 fn try_from_lenc(val: String) -> Result<Self, &'static str> {
127 if val.len() != 40 {
128 return Err("LENC String provided to LtkInfo is not the right size");
129 }
130
131 Ok(LtkInfo {
132 key: u128::from_str_radix(&val[0..32], 16).unwrap_or_default(),
133 ediv: u16::from_str_radix(&val[32..36], 16).unwrap_or_default().swap_bytes(),
134 len: u8::from_str_radix(&val[36..38], 16).unwrap_or_default(),
135 auth: u8::from_str_radix(&val[38..40], 16).unwrap_or_default(),
136 rand: 0,
137 })
138 }
139
try_into_lenc(self) -> Result<String, &'static str>140 fn try_into_lenc(self) -> Result<String, &'static str> {
141 Ok(format!(
142 "{:032x}{:04x}{:02x}{:02x}",
143 self.key,
144 self.ediv.swap_bytes(),
145 self.len,
146 self.auth,
147 ))
148 }
149 }
150
151 /// Represents the different conversions that can be done on keys
152 pub enum Converter {
153 HexToDec,
154 DecToHex,
155 Base64ToHex,
156 HexToBase64,
157
158 TypeB2F,
159 TypeF2B,
160 AddrTypeB2F,
161 AddrTypeF2B,
162
163 ReverseEndianLowercase,
164 ReverseEndianUppercase,
165
166 ReplaceSemiColonWithSpace,
167 ReplaceSpaceWithSemiColon,
168 }
169
170 /// Represents the different actions to perform on a DeviceKey
171 pub enum KeyAction {
172 WrapOk,
173 Apply(Converter),
174 ToSection(&'static str),
175 ApplyToSection(Converter, &'static str),
176 }
177
178 pub type DeviceKeyError = String;
179
180 /// Represents required info needed to convert keys between Floss and BlueZ
181 struct DeviceKey {
182 pub key: &'static str,
183 action: KeyAction,
184 // Needed in Floss to BlueZ conversion
185 pub section: &'static str,
186 }
187
188 impl DeviceKey {
189 /// Returns a DeviceKey with the key and action given
new(key: &'static str, action: KeyAction) -> Self190 fn new(key: &'static str, action: KeyAction) -> Self {
191 Self { key, action, section: "" }
192 }
193
194 /// Performs the KeyAction stored and returns the result of the key conversion
apply_action(&mut self, value: String) -> Result<String, DeviceKeyError>195 fn apply_action(&mut self, value: String) -> Result<String, DeviceKeyError> {
196 // Helper function to do the actual conversion
197 fn apply_conversion(conv: &Converter, value: String) -> Result<String, DeviceKeyError> {
198 match conv {
199 Converter::HexToDec => hex_str_to_dec_str(value),
200 Converter::DecToHex => dec_str_to_hex_str(value),
201 Converter::Base64ToHex => base64_str_to_hex_str(value),
202 Converter::HexToBase64 => hex_str_to_base64_str(value),
203 Converter::TypeB2F => bluez_to_floss_type(value),
204 Converter::TypeF2B => floss_to_bluez_type(value),
205 Converter::AddrTypeB2F => bluez_to_floss_addr_type(value),
206 Converter::AddrTypeF2B => floss_to_bluez_addr_type(value),
207 Converter::ReverseEndianLowercase => reverse_endianness(value, false),
208 Converter::ReverseEndianUppercase => reverse_endianness(value, true),
209 Converter::ReplaceSemiColonWithSpace => Ok(value.replace(';', " ")),
210 Converter::ReplaceSpaceWithSemiColon => Ok(value.replace(' ', ";")),
211 }
212 }
213
214 match &self.action {
215 KeyAction::WrapOk => Ok(value),
216 KeyAction::Apply(converter) => apply_conversion(converter, value),
217 KeyAction::ToSection(sec) => {
218 self.section = sec;
219 Ok(value)
220 }
221 KeyAction::ApplyToSection(converter, sec) => {
222 self.section = sec;
223 apply_conversion(converter, value)
224 }
225 }
226 }
227 }
228
hex_str_to_dec_str(str: String) -> Result<String, String>229 fn hex_str_to_dec_str(str: String) -> Result<String, String> {
230 match u32::from_str_radix(str.trim_start_matches("0x"), 16) {
231 Ok(str) => Ok(format!("{}", str)),
232 Err(err) => Err(format!("Error converting from hex string to dec string: {}", err)),
233 }
234 }
235
dec_str_to_hex_str(str: String) -> Result<String, String>236 fn dec_str_to_hex_str(str: String) -> Result<String, String> {
237 match str.parse::<u32>() {
238 Ok(x) => Ok(format!("0x{:X}", x)),
239 Err(err) => Err(format!("Error converting from dec string to hex string: {}", err)),
240 }
241 }
242
base64_str_to_hex_str(str: String) -> Result<String, String>243 fn base64_str_to_hex_str(str: String) -> Result<String, String> {
244 match base64::decode(str) {
245 Ok(bytes) => {
246 let res: String = bytes.iter().fold(String::new(), |mut res, b| {
247 let _ = write!(res, "{:02x}", b);
248 res
249 });
250 Ok(res)
251 }
252 Err(err) => Err(format!("Error converting from base64 string to hex string: {}", err)),
253 }
254 }
255
hex_str_to_base64_str(str: String) -> Result<String, String>256 fn hex_str_to_base64_str(str: String) -> Result<String, String> {
257 // Make vector of bytes from octets
258 let mut bytes = Vec::new();
259 for i in 0..(str.len() / 2) {
260 let res = u8::from_str_radix(&str[2 * i..2 * i + 2], 16);
261 match res {
262 Ok(v) => bytes.push(v),
263 Err(err) => {
264 return Err(format!("Error converting from hex string to base64 string: {}", err));
265 }
266 }
267 }
268
269 Ok(base64::encode(&bytes))
270 }
271
bluez_to_floss_type(str: String) -> Result<String, String>272 fn bluez_to_floss_type(str: String) -> Result<String, String> {
273 match str.as_str() {
274 CLASSIC_TYPE => Ok("1".into()),
275 LE_TYPE => Ok("2".into()),
276 DUAL_TYPE => Ok("3".into()),
277 x => Err(format!("Error converting type. Unknown type: {}", x)),
278 }
279 }
280
floss_to_bluez_type(str: String) -> Result<String, String>281 fn floss_to_bluez_type(str: String) -> Result<String, String> {
282 match str.as_str() {
283 "1" => Ok(CLASSIC_TYPE.into()),
284 "2" => Ok(LE_TYPE.into()),
285 "3" => Ok(DUAL_TYPE.into()),
286 x => Err(format!("Error converting type. Unknown type: {}", x)),
287 }
288 }
289
bluez_to_floss_addr_type(str: String) -> Result<String, String>290 fn bluez_to_floss_addr_type(str: String) -> Result<String, String> {
291 match str.as_str() {
292 "public" => Ok("0".into()),
293 "static" => Ok("1".into()),
294 x => Err(format!("Error converting address type. Unknown type: {}", x)),
295 }
296 }
297
floss_to_bluez_addr_type(str: String) -> Result<String, String>298 fn floss_to_bluez_addr_type(str: String) -> Result<String, String> {
299 match str.as_str() {
300 "0" => Ok("public".into()),
301 "1" => Ok("static".into()),
302 x => Err(format!("Error converting address type. Unknown type: {}", x)),
303 }
304 }
305
306 // BlueZ stores link keys as little endian and Floss as big endian
reverse_endianness(str: String, uppercase: bool) -> Result<String, String>307 fn reverse_endianness(str: String, uppercase: bool) -> Result<String, String> {
308 match u128::from_str_radix(str.as_str(), 16) {
309 Ok(x) => {
310 if uppercase {
311 Ok(format!("{:0>32X}", x.swap_bytes()))
312 } else {
313 Ok(format!("{:0>32x}", x.swap_bytes()))
314 }
315 }
316 Err(err) => Err(format!("Error converting link key: {}", err)),
317 }
318 }
319
320 /// Helper function that does the conversion from BlueZ to Floss for a single device
321 ///
322 /// # Arguments
323 /// * `filename` - A string slice that holds the path of the BlueZ file to get info from
324 /// * `addr` - A string slice that holds the address of the BlueZ device that we're converting
325 /// * `floss_conf` - The Floss Ini that we're adding to
326 /// * `is_hid_file` - Whether the file is a BlueZ hog-uhid-cache file or BlueZ info file
327 ///
328 /// # Returns
329 /// Whether the conversion was successful or not
convert_from_bluez_device( filename: &str, addr: &str, floss_conf: &mut Ini, is_hid_file: bool, ) -> bool330 fn convert_from_bluez_device(
331 filename: &str,
332 addr: &str,
333 floss_conf: &mut Ini,
334 is_hid_file: bool,
335 ) -> bool {
336 // Floss device address strings need to be lower case
337 let addr_lower = addr.to_lowercase();
338
339 let mut bluez_conf = Ini::new_cs();
340 // Default Ini uses ";" and "#" for comments
341 bluez_conf.set_comment_symbols(&['!', '#']);
342 let bluez_map = match bluez_conf.load(filename) {
343 Ok(map) => map,
344 Err(err) => {
345 error!(
346 "Error converting BlueZ conf to Floss conf: {}. Dropping conversion for device {}",
347 err, addr
348 );
349 floss_conf.remove_section(addr_lower.as_str());
350 return false;
351 }
352 };
353
354 // Floss will not load the HID info unless it sees this key and BlueZ does not have a matching key
355 if is_hid_file {
356 floss_conf.set(addr_lower.as_str(), "HidAttrMask", Some("0".into()));
357 }
358
359 for (sec, props) in bluez_map {
360 // Special handling for LE keys since in Floss they are a combination of values in BlueZ
361 let handled = match sec.as_str() {
362 IRK_SECTION_NAME => {
363 // In Floss, LE_KEY_PID = IRK + Identity Address Type (8) + Identity Address
364 let irk = reverse_endianness(
365 bluez_conf.get(sec.as_str(), "Key").unwrap_or_default(),
366 false,
367 )
368 .unwrap_or_default();
369 let addr_type = bluez_to_floss_addr_type(
370 bluez_conf.get(GENERAL_SECTION_NAME, "AddressType").unwrap_or_default(),
371 )
372 .unwrap_or_default()
373 .parse::<u8>()
374 .unwrap_or_default();
375 floss_conf.set(
376 addr_lower.as_str(),
377 "LE_KEY_PID",
378 Some(format!("{}{:02x}{}", irk, addr_type, addr_lower.replace(':', ""))),
379 );
380 true
381 }
382 BLUEZ_PERIPHERAL_LTK_SECTION_NAME | LTK_SECTION_NAME => {
383 // Special handling since in Floss LE_KEY_PENC is a combination of values in BlueZ
384 let ltk: LtkInfo = LtkInfo::new_from_bluez(&bluez_conf, sec.as_str());
385 floss_conf.set(
386 addr_lower.as_str(),
387 "LE_KEY_PENC",
388 Some(ltk.try_into_penc().unwrap_or_default()),
389 );
390 true
391 }
392 BLUEZ_LOCAL_LTK_SECTION_NAME => {
393 // Special handling since in Floss LE_KEY_LENC is a combination of values in BlueZ
394 let lltk: LtkInfo = LtkInfo::new_from_bluez(&bluez_conf, sec.as_str());
395 floss_conf.set(
396 addr_lower.as_str(),
397 "LE_KEY_LENC",
398 Some(lltk.try_into_lenc().unwrap_or_default()),
399 );
400 true
401 }
402 _ => false,
403 };
404
405 if handled {
406 continue;
407 }
408
409 let mut map: HashMap<&str, Vec<DeviceKey>> = if is_hid_file {
410 match sec.as_str() {
411 REPORT_MAP_SECTION_NAME => [(
412 "report_map",
413 vec![DeviceKey::new("HidDescriptor", KeyAction::Apply(Converter::Base64ToHex))],
414 )]
415 .into(),
416 GENERAL_SECTION_NAME => [
417 ("bcdhid", vec![DeviceKey::new("HidVersion", KeyAction::WrapOk)]),
418 ("bcountrycode", vec![DeviceKey::new("HidCountryCode", KeyAction::WrapOk)]),
419 ]
420 .into(),
421 _ => [].into(),
422 }
423 } else {
424 // info file
425 match sec.as_str() {
426 GENERAL_SECTION_NAME => [
427 ("Name", vec![DeviceKey::new("Name", KeyAction::WrapOk)]),
428 (
429 "Class",
430 vec![DeviceKey::new("DevClass", KeyAction::Apply(Converter::HexToDec))],
431 ),
432 (
433 "Appearance",
434 vec![DeviceKey::new("Appearance", KeyAction::Apply(Converter::HexToDec))],
435 ),
436 (
437 "SupportedTechnologies",
438 vec![DeviceKey::new("DevType", KeyAction::Apply(Converter::TypeB2F))],
439 ),
440 (
441 "Services",
442 vec![DeviceKey::new(
443 "Service",
444 KeyAction::Apply(Converter::ReplaceSemiColonWithSpace),
445 )],
446 ),
447 (
448 "AddressType",
449 vec![DeviceKey::new("AddrType", KeyAction::Apply(Converter::AddrTypeB2F))],
450 ),
451 ]
452 .into(),
453 LINKKEY_SECTION_NAME => [
454 (
455 "Key",
456 vec![DeviceKey::new(
457 "LinkKey",
458 KeyAction::Apply(Converter::ReverseEndianLowercase),
459 )],
460 ),
461 ("Type", vec![DeviceKey::new("LinkKeyType", KeyAction::WrapOk)]),
462 ("PINLength", vec![DeviceKey::new("PinLength", KeyAction::WrapOk)]),
463 ]
464 .into(),
465 DEVICEID_SECTION_NAME => [
466 (
467 "Source",
468 vec![
469 DeviceKey::new("SdpDiVendorIdSource", KeyAction::WrapOk),
470 DeviceKey::new("VendorIdSource", KeyAction::WrapOk),
471 ],
472 ),
473 (
474 "Vendor",
475 vec![
476 DeviceKey::new("SdpDiManufacturer", KeyAction::WrapOk),
477 DeviceKey::new("VendorId", KeyAction::WrapOk),
478 ],
479 ),
480 (
481 "Product",
482 vec![
483 DeviceKey::new("SdpDiModel", KeyAction::WrapOk),
484 DeviceKey::new("ProductId", KeyAction::WrapOk),
485 ],
486 ),
487 (
488 "Version",
489 vec![
490 DeviceKey::new("SdpDiHardwareVersion", KeyAction::WrapOk),
491 DeviceKey::new("ProductVersion", KeyAction::WrapOk),
492 ],
493 ),
494 ]
495 .into(),
496 _ => [].into(),
497 }
498 };
499
500 // Do the conversion for all keys found in BlueZ
501 for (k, v) in props {
502 match map.get_mut(k.as_str()) {
503 Some(keys) => {
504 for key in keys {
505 let new_val = match key.apply_action(v.clone().unwrap_or_default()) {
506 Ok(val) => val,
507 Err(err) => {
508 error!(
509 "Error converting BlueZ conf to Floss conf: {}. \
510 Dropping conversion for device {}",
511 err, addr
512 );
513 floss_conf.remove_section(addr_lower.as_str());
514 return false;
515 }
516 };
517 floss_conf.set(addr_lower.as_str(), key.key, Some(new_val));
518 }
519 }
520 None => {
521 debug!("No key match: {}", k);
522 }
523 }
524 }
525 }
526
527 true
528 }
529
530 /// This is the main function that handles the device migration from BlueZ to Floss.
migrate_bluez_devices()531 pub fn migrate_bluez_devices() {
532 // Maps adapter address to Ini
533 let mut adapter_conf_map: HashMap<String, Ini> = HashMap::new();
534
535 // Find and parse all device files
536 // In BlueZ, device info files look like /var/lib/bluetooth/<adapter address>/<device address>/info
537 let globbed = match glob(format!("{}/*:*/*:*/info", BT_LIBDIR).as_str()) {
538 Ok(v) => v,
539 Err(_) => {
540 warn!("Didn't find any BlueZ adapters to migrate");
541 return;
542 }
543 };
544 for entry in globbed {
545 let info_path = entry.unwrap_or_default();
546 let hid_path = info_path.to_str().unwrap_or_default().replace("info", "hog-uhid-cache");
547 let addrs = info_path.to_str().unwrap_or_default().split('/').collect::<Vec<&str>>();
548 let adapter_addr = addrs[addrs.len() - 3];
549 let device_addr = addrs[addrs.len() - 2];
550 // Create new Ini file if it doesn't already exist
551 adapter_conf_map.entry(adapter_addr.into()).or_insert(Ini::new_cs());
552 if !convert_from_bluez_device(
553 info_path.to_str().unwrap_or_default(),
554 device_addr,
555 adapter_conf_map.get_mut(adapter_addr).unwrap_or(&mut Ini::new_cs()),
556 /*is_hid_file=*/ false,
557 ) {
558 continue;
559 }
560
561 // Check if we have HID info
562 if Path::new(hid_path.as_str()).exists() {
563 convert_from_bluez_device(
564 hid_path.as_str(),
565 device_addr,
566 adapter_conf_map.get_mut(adapter_addr).unwrap_or(&mut Ini::new_cs()),
567 /*is_hid_file=*/ true,
568 );
569 }
570 }
571
572 // Write migration to appropriate adapter files
573 // TODO(b/232138101): Update for multi-adapter support
574 for (adapter, conf) in adapter_conf_map.iter_mut() {
575 let mut existing_conf = Ini::new_cs();
576 match existing_conf.load(FLOSS_CONF_FILE) {
577 Ok(ini) => {
578 let devices = conf.sections();
579 for (sec, props) in ini {
580 // Drop devices that don't exist in BlueZ
581 if sec.contains(':') && !devices.contains(&sec) {
582 info!("Dropping a device in Floss that doesn't exist in BlueZ");
583 continue;
584 }
585 // Keep keys that weren't transferrable
586 for (k, v) in props {
587 if conf.get(sec.as_str(), k.as_str()).is_none() {
588 conf.set(sec.as_str(), k.as_str(), v);
589 }
590 }
591 }
592 }
593 // Conf file doesn't exist yet
594 Err(_) => {
595 conf.set(ADAPTER_SECTION_NAME, "Address", Some(adapter.clone()));
596 }
597 }
598 // Write contents to file
599 match conf.write(FLOSS_CONF_FILE) {
600 Ok(_) => {
601 info!("Successfully migrated devices from BlueZ to Floss for adapter {}", adapter);
602 }
603 Err(err) => {
604 error!(
605 "Error migrating devices from BlueZ to Floss for adapter {}: {}",
606 adapter, err
607 );
608 }
609 }
610 }
611 }
612
613 /// Helper function in Floss to BlueZ conversion that takes a Floss device that already
614 /// exists in BlueZ and keeps keys that weren't available from Floss conf file. Then
615 /// writes to BlueZ file to complete device migration.
616 ///
617 /// # Arguments
618 /// * `filepath` - A string that holds the path of the BlueZ info file
619 /// * `conf` - BlueZ Ini file that contains migrated Floss device
merge_and_write_bluez_conf(filepath: String, conf: &mut Ini)620 fn merge_and_write_bluez_conf(filepath: String, conf: &mut Ini) {
621 let mut existing_conf = Ini::new_cs();
622 existing_conf.set_comment_symbols(&['!', '#']);
623 if let Ok(ini) = existing_conf.load(filepath.clone()) {
624 // Device already exists in BlueZ
625 for (sec, props) in ini {
626 // Keep keys that weren't transferrable
627 for (k, v) in props {
628 if conf.get(sec.as_str(), k.as_str()).is_none() {
629 conf.set(sec.as_str(), k.as_str(), v);
630 }
631 }
632 }
633 }
634 // Write BlueZ file
635 match conf.write(filepath.clone()) {
636 Ok(_) => {
637 info!("Successfully migrated Floss to BlueZ: {}", filepath);
638 }
639 Err(err) => {
640 error!("Error writing Floss to BlueZ: {}: {}", filepath, err);
641 }
642 }
643 }
644
645 /// Helper function that does the conversion from Floss to BlueZ for a single adapter
646 ///
647 /// # Arguments
648 /// * `filename` - A string slice that holds the path of the Floss conf file to get device info from
convert_floss_conf(filename: &str)649 fn convert_floss_conf(filename: &str) {
650 let mut floss_conf = Ini::new_cs();
651 let floss_map = match floss_conf.load(filename) {
652 Ok(map) => map,
653 Err(err) => {
654 warn!(
655 "Error opening ini file while converting Floss to BlueZ for {}: {}",
656 filename, err
657 );
658 return;
659 }
660 };
661
662 let adapter_addr = match floss_conf.get(ADAPTER_SECTION_NAME, "Address") {
663 Some(addr) => addr.to_uppercase(),
664 None => {
665 warn!("No adapter address during Floss to BlueZ migration in {}", filename);
666 return;
667 }
668 };
669
670 // BlueZ info file map
671 let mut info_map: HashMap<&str, DeviceKey> = [
672 // General
673 ("Name", DeviceKey::new("Name", KeyAction::ToSection(GENERAL_SECTION_NAME))),
674 (
675 "DevClass",
676 DeviceKey::new(
677 "Class",
678 KeyAction::ApplyToSection(Converter::DecToHex, GENERAL_SECTION_NAME),
679 ),
680 ),
681 (
682 "Appearance",
683 DeviceKey::new(
684 "Appearance",
685 KeyAction::ApplyToSection(Converter::DecToHex, GENERAL_SECTION_NAME),
686 ),
687 ),
688 (
689 "DevType",
690 DeviceKey::new(
691 "SupportedTechnologies",
692 KeyAction::ApplyToSection(Converter::TypeF2B, GENERAL_SECTION_NAME),
693 ),
694 ),
695 (
696 "Service",
697 DeviceKey::new(
698 "Services",
699 KeyAction::ApplyToSection(
700 Converter::ReplaceSpaceWithSemiColon,
701 GENERAL_SECTION_NAME,
702 ),
703 ),
704 ),
705 (
706 "AddrType",
707 DeviceKey::new(
708 "AddressType",
709 KeyAction::ApplyToSection(Converter::AddrTypeF2B, GENERAL_SECTION_NAME),
710 ),
711 ),
712 // LinkKey
713 (
714 "LinkKey",
715 DeviceKey::new(
716 "Key",
717 KeyAction::ApplyToSection(Converter::ReverseEndianUppercase, LINKKEY_SECTION_NAME),
718 ),
719 ),
720 ("LinkKeyType", DeviceKey::new("Type", KeyAction::ToSection(LINKKEY_SECTION_NAME))),
721 ("PinLength", DeviceKey::new("PINLength", KeyAction::ToSection(LINKKEY_SECTION_NAME))),
722 // DeviceID
723 ("VendorIdSource", DeviceKey::new("Source", KeyAction::ToSection(DEVICEID_SECTION_NAME))),
724 ("VendorId", DeviceKey::new("Vendor", KeyAction::ToSection(DEVICEID_SECTION_NAME))),
725 ("ProductId", DeviceKey::new("Product", KeyAction::ToSection(DEVICEID_SECTION_NAME))),
726 ("ProductVersion", DeviceKey::new("Version", KeyAction::ToSection(DEVICEID_SECTION_NAME))),
727 ]
728 .into();
729
730 // BlueZ hog-uhid-cache file map
731 let mut hid_map: HashMap<&str, DeviceKey> = [
732 // General
733 ("HidVersion", DeviceKey::new("bcdhid", KeyAction::ToSection(GENERAL_SECTION_NAME))),
734 (
735 "HidCountryCode",
736 DeviceKey::new("bcountrycode", KeyAction::ToSection(GENERAL_SECTION_NAME)),
737 ),
738 // ReportMap
739 (
740 "HidDescriptor",
741 DeviceKey::new(
742 "report_map",
743 KeyAction::ApplyToSection(Converter::HexToBase64, REPORT_MAP_SECTION_NAME),
744 ),
745 ),
746 ]
747 .into();
748
749 let mut devices: Vec<String> = Vec::new();
750 for (sec, props) in floss_map {
751 // Skip all the non-adapter sections
752 if !sec.contains(':') {
753 continue;
754 }
755 // Keep track of Floss devices we've seen so we can remove BlueZ devices that don't exist on Floss
756 devices.push(sec.clone());
757 let mut device_addr = sec.to_uppercase();
758 let mut bluez_info = Ini::new_cs();
759 let mut bluez_hid = Ini::new_cs();
760 let mut is_hid: bool = false;
761 let has_local_keys: bool = props.contains_key("LE_KEY_LENC");
762 for (k, v) in props {
763 // Special handling since in Floss LE_KEY_* are a combination of values in BlueZ
764 if k == "LE_KEY_PID" {
765 // In Floss, LE_KEY_PID = IRK (128) + Identity Address Type (8) + Identity Address (48)
766 // Identity Address Type is also found as "AddrType" key so no need to parse it here.
767 let val = v.unwrap_or_default();
768 if val.len() != 46 {
769 warn!("LE_KEY_PID is not the correct size: {}", val);
770 continue;
771 }
772 bluez_info.set(
773 IRK_SECTION_NAME,
774 "Key",
775 Some(reverse_endianness(val[0..32].to_string(), true).unwrap_or_default()),
776 );
777 // BlueZ uses the identity address as the device path
778 devices.retain(|d| *d != device_addr);
779 device_addr = format!(
780 "{}:{}:{}:{}:{}:{}",
781 &val[34..36],
782 &val[36..38],
783 &val[38..40],
784 &val[40..42],
785 &val[42..44],
786 &val[44..46]
787 )
788 .to_uppercase();
789 devices.push(device_addr.clone().to_lowercase());
790 continue;
791 } else if k == "LE_KEY_PENC" {
792 let ltk = LtkInfo::try_from_penc(v.unwrap_or_default()).unwrap_or_default();
793 let section_name = if has_local_keys {
794 BLUEZ_PERIPHERAL_LTK_SECTION_NAME
795 } else {
796 LTK_SECTION_NAME
797 };
798 bluez_info.set(section_name, "Key", Some(format!("{:032X}", ltk.key)));
799 bluez_info.set(section_name, "Rand", Some(format!("{}", ltk.rand)));
800 bluez_info.set(section_name, "EDiv", Some(format!("{}", ltk.ediv)));
801 bluez_info.set(
802 section_name,
803 "Authenticated",
804 Some(format!("{}", LtkInfo::auth_to_bluez(ltk.auth))),
805 );
806 bluez_info.set(section_name, "EncSize", Some(format!("{}", ltk.len)));
807 continue;
808 } else if k == "LE_KEY_LENC" {
809 let ltk = LtkInfo::try_from_lenc(v.unwrap_or_default()).unwrap_or_default();
810 bluez_info.set(
811 BLUEZ_LOCAL_LTK_SECTION_NAME,
812 "Key",
813 Some(format!("{:032X}", ltk.key)),
814 );
815 bluez_info.set(BLUEZ_LOCAL_LTK_SECTION_NAME, "Rand", Some(format!("{}", ltk.rand)));
816 bluez_info.set(BLUEZ_LOCAL_LTK_SECTION_NAME, "EDiv", Some(format!("{}", ltk.ediv)));
817 bluez_info.set(
818 BLUEZ_LOCAL_LTK_SECTION_NAME,
819 "Authenticated",
820 Some(format!("{}", LtkInfo::auth_to_bluez(ltk.auth))),
821 );
822 bluez_info.set(
823 BLUEZ_LOCAL_LTK_SECTION_NAME,
824 "EncSize",
825 Some(format!("{}", ltk.len)),
826 );
827 continue;
828 }
829 // Convert matching info file keys
830 match info_map.get_mut(k.as_str()) {
831 Some(key) => {
832 let new_val = match key.apply_action(v.unwrap_or_default()) {
833 Ok(val) => val,
834 Err(err) => {
835 warn!("Error converting Floss to Bluez key for adapter {}, device {}, key {}: {}", adapter_addr, device_addr, k, err);
836 continue;
837 }
838 };
839 bluez_info.set(key.section, key.key, Some(new_val));
840 continue;
841 }
842 None => {
843 debug!("No key match: {}", k)
844 }
845 }
846 // Convert matching hog-uhid-cache file keys
847 match hid_map.get_mut(k.as_str()) {
848 Some(key) => {
849 is_hid = true;
850 let new_val = match key.apply_action(v.unwrap_or_default()) {
851 Ok(val) => val,
852 Err(err) => {
853 warn!("Error converting Floss to Bluez key for adapter {}, device {}, key {}: {}", adapter_addr, device_addr, k, err);
854 continue;
855 }
856 };
857 bluez_hid.set(key.section, key.key, Some(new_val));
858 }
859 None => {
860 debug!("No key match: {}", k)
861 }
862 }
863 }
864
865 let path = format!("{}/{}/{}", BT_LIBDIR, adapter_addr, device_addr);
866
867 // Create BlueZ device dir and all its parents if they're missing
868 match fs::create_dir_all(path.clone()) {
869 Ok(_) => (),
870 Err(err) => {
871 error!("Error creating dirs during Floss to BlueZ device migration for adapter{}, device {}: {}", adapter_addr, device_addr, err);
872 }
873 }
874 // Write info file
875 merge_and_write_bluez_conf(format!("{}/{}", path, "info"), &mut bluez_info);
876
877 // Write hog-uhid-cache file
878 if is_hid {
879 merge_and_write_bluez_conf(format!("{}/{}", path, "hog-uhid-cache"), &mut bluez_hid);
880 }
881 }
882
883 // Delete devices that exist in BlueZ but not in Floss
884 if let Ok(globbed) = glob(format!("{}/{}/*:*", BT_LIBDIR, adapter_addr).as_str()) {
885 for entry in globbed {
886 let pathbuf = entry.unwrap_or_default();
887 let addrs = pathbuf.to_str().unwrap_or_default().split('/').collect::<Vec<&str>>();
888 let device_addr: String = addrs[addrs.len() - 1].into();
889 if !devices.contains(&device_addr.to_lowercase()) {
890 if let Err(err) = fs::remove_dir_all(pathbuf) {
891 warn!(
892 "Error removing {} during Floss to BlueZ device migration: {}",
893 device_addr, err
894 );
895 }
896 }
897 }
898 }
899 }
900
901 /// This is the main function that handles the device migration from Floss to BlueZ.
migrate_floss_devices()902 pub fn migrate_floss_devices() {
903 // Find and parse all Floss conf files
904 // TODO(b/232138101): Currently Floss only supports a single adapter; update here for multi-adapter support
905 let globbed = match glob(FLOSS_CONF_FILE) {
906 Ok(v) => v,
907 Err(_) => {
908 warn!("Didn't find Floss conf file to migrate");
909 return;
910 }
911 };
912
913 for entry in globbed {
914 convert_floss_conf(entry.unwrap_or_default().to_str().unwrap_or_default());
915 }
916 }
917
918 #[cfg(test)]
919 mod tests {
920 use super::*;
921
922 #[test]
test_device_key_wrapok()923 fn test_device_key_wrapok() {
924 let test_str = String::from("do_nothing");
925 let mut key = DeviceKey::new("", KeyAction::WrapOk);
926 assert_eq!(key.apply_action(test_str.clone()), Ok(test_str));
927 }
928
929 #[test]
test_device_key_to_section()930 fn test_device_key_to_section() {
931 let test_str = String::from("do_nothing");
932 let mut key = DeviceKey::new("", KeyAction::ToSection(LINKKEY_SECTION_NAME));
933 assert_eq!(key.apply_action(test_str.clone()), Ok(test_str));
934 assert_eq!(key.section, LINKKEY_SECTION_NAME)
935 }
936
937 #[test]
test_device_key_apply_dec_to_hex()938 fn test_device_key_apply_dec_to_hex() {
939 // DevClass example
940 let mut key = DeviceKey::new("", KeyAction::Apply(Converter::DecToHex));
941 assert_eq!(key.apply_action("2360344".to_string()), Ok("0x240418".to_string()));
942 assert_eq!(
943 key.apply_action("236034B".to_string()),
944 Err("Error converting from dec string to hex string: invalid digit found in string"
945 .to_string())
946 );
947 }
948
949 #[test]
test_device_key_apply_to_section_hex_to_dec()950 fn test_device_key_apply_to_section_hex_to_dec() {
951 // DevClass example
952 let mut key = DeviceKey::new(
953 "",
954 KeyAction::ApplyToSection(Converter::HexToDec, GENERAL_SECTION_NAME),
955 );
956 assert_eq!(key.apply_action("0x240418".to_string()), Ok("2360344".to_string()));
957 assert_eq!(key.section, GENERAL_SECTION_NAME);
958 assert_eq!(
959 key.apply_action("236034T".to_string()),
960 Err("Error converting from hex string to dec string: invalid digit found in string"
961 .to_string())
962 );
963 }
964
965 #[test]
test_hex_to_base64()966 fn test_hex_to_base64() {
967 // HID report map example taken from real HID mouse conversion
968 let mut key = DeviceKey::new("", KeyAction::Apply(Converter::HexToBase64));
969 assert_eq!(
970 key.apply_action("05010906a1018501050719e029e71500250175019508810295067508150026a400050719002aa4008100c005010902a10185020901a1009510750115002501050919012910810205011601f826ff07750c95020930093181061581257f75089501093881069501050c0a38028106c0c00643ff0a0202a101851175089513150026ff000902810009029100c0".to_string()),
971 Ok("BQEJBqEBhQEFBxngKecVACUBdQGVCIEClQZ1CBUAJqQABQcZACqkAIEAwAUBCQKhAYUCCQGhAJUQdQEVACUBBQkZASkQgQIFARYB+Cb/B3UMlQIJMAkxgQYVgSV/dQiVAQk4gQaVAQUMCjgCgQbAwAZD/woCAqEBhRF1CJUTFQAm/wAJAoEACQKRAMA=".to_string())
972 );
973 assert_eq!(
974 key.apply_action("x5010906a1018501050719e029e71500250175019508810295067508150026a400050719002aa4008100c005010902a10185020901a1009510750115002501050919012910810205011601f826ff07750c95020930093181061581257f75089501093881069501050c0a38028106c0c00643ff0a0202a101851175089513150026ff000902810009029100c0".to_string()),
975 Err("Error converting from hex string to base64 string: invalid digit found in string".to_string())
976 );
977 }
978
979 #[test]
test_hex_to_base64_to_hex()980 fn test_hex_to_base64_to_hex() {
981 // HID report map example taken from real HID mouse conversion
982 let mut key = DeviceKey::new("", KeyAction::Apply(Converter::Base64ToHex));
983 assert_eq!(
984 key.apply_action("BQEJBqEBhQEFBxngKecVACUBdQGVCIEClQZ1CBUAJqQABQcZACqkAIEAwAUBCQKhAYUCCQGhAJUQdQEVACUBBQkZASkQgQIFARYB+Cb/B3UMlQIJMAkxgQYVgSV/dQiVAQk4gQaVAQUMCjgCgQbAwAZD/woCAqEBhRF1CJUTFQAm/wAJAoEACQKRAMA=".to_string()),
985 Ok("05010906a1018501050719e029e71500250175019508810295067508150026a400050719002aa4008100c005010902a10185020901a1009510750115002501050919012910810205011601f826ff07750c95020930093181061581257f75089501093881069501050c0a38028106c0c00643ff0a0202a101851175089513150026ff000902810009029100c0".to_string())
986 );
987 assert_eq!(
988 key.apply_action("!BQEJBqEBhQEFBxngKecVACUBdQGVCIEClQZ1CBUAJqQABQcZACqkAIEAwAUBCQKhAYUCCQGhAJUQdQEVACUBBQkZASkQgQIFARYB+Cb/B3UMlQIJMAkxgQYVgSV/dQiVAQk4gQaVAQUMCjgCgQbAwAZD/woCAqEBhRF1CJUTFQAm/wAJAoEACQKRAMA=".to_string()),
989 Err("Error converting from base64 string to hex string: Encoded text cannot have a 6-bit remainder.".to_string())
990 );
991 }
992
993 #[test]
test_typeb2f()994 fn test_typeb2f() {
995 let mut key = DeviceKey::new("", KeyAction::Apply(Converter::TypeB2F));
996 assert_eq!(key.apply_action(CLASSIC_TYPE.to_string()), Ok("1".to_string()));
997 assert_eq!(key.apply_action(LE_TYPE.to_string()), Ok("2".to_string()));
998 assert_eq!(key.apply_action(DUAL_TYPE.to_string()), Ok("3".to_string()));
999 assert_eq!(
1000 key.apply_action("FAKE_TYPE".to_string()),
1001 Err("Error converting type. Unknown type: FAKE_TYPE".to_string())
1002 );
1003 }
1004
1005 #[test]
test_typef2b()1006 fn test_typef2b() {
1007 let mut key = DeviceKey::new("", KeyAction::Apply(Converter::TypeF2B));
1008 assert_eq!(key.apply_action("1".to_string()), Ok(CLASSIC_TYPE.to_string()));
1009 assert_eq!(key.apply_action("2".to_string()), Ok(LE_TYPE.to_string()));
1010 assert_eq!(key.apply_action("3".to_string()), Ok(DUAL_TYPE.to_string()));
1011 assert_eq!(
1012 key.apply_action("FAKE_TYPE".to_string()),
1013 Err("Error converting type. Unknown type: FAKE_TYPE".to_string())
1014 );
1015 }
1016
1017 #[test]
test_addrtypeb2f()1018 fn test_addrtypeb2f() {
1019 let mut key = DeviceKey::new("", KeyAction::Apply(Converter::AddrTypeB2F));
1020 assert_eq!(key.apply_action("public".to_string()), Ok("0".to_string()));
1021 assert_eq!(key.apply_action("static".to_string()), Ok("1".to_string()));
1022 assert_eq!(
1023 key.apply_action("FAKE_TYPE".to_string()),
1024 Err("Error converting address type. Unknown type: FAKE_TYPE".to_string())
1025 );
1026 }
1027
1028 #[test]
test_addrtypef2b()1029 fn test_addrtypef2b() {
1030 let mut key = DeviceKey::new("", KeyAction::Apply(Converter::AddrTypeF2B));
1031 assert_eq!(key.apply_action("0".to_string()), Ok("public".to_string()));
1032 assert_eq!(key.apply_action("1".to_string()), Ok("static".to_string()));
1033 assert_eq!(
1034 key.apply_action("FAKE_TYPE".to_string()),
1035 Err("Error converting address type. Unknown type: FAKE_TYPE".to_string())
1036 );
1037 }
1038
1039 #[test]
test_reverseendian()1040 fn test_reverseendian() {
1041 let mut key = DeviceKey::new("", KeyAction::Apply(Converter::ReverseEndianLowercase));
1042 assert_eq!(
1043 key.apply_action("00112233445566778899AABBCCDDEEFF".to_string()),
1044 Ok("ffeeddccbbaa99887766554433221100".to_string())
1045 );
1046 // Link key too small shouldn't panic
1047 assert_eq!(
1048 key.apply_action("00112233445566778899AABBCCDDEE".to_string()),
1049 Ok("eeddccbbaa9988776655443322110000".to_string())
1050 );
1051 // Conversion shouldn't lose leading zeros
1052 assert_eq!(
1053 key.apply_action("112233445566778899AABBCCDDEE0000".to_string()),
1054 Ok("0000eeddccbbaa998877665544332211".to_string())
1055 );
1056 }
1057
1058 #[test]
test_replacespacewithsemicolon()1059 fn test_replacespacewithsemicolon() {
1060 // UUID example
1061 let mut key = DeviceKey::new("", KeyAction::Apply(Converter::ReplaceSpaceWithSemiColon));
1062 assert_eq!(
1063 key.apply_action(
1064 "00001800-0000-1000-8000-00805f9b34fb 00001801-0000-1000-8000-00805f9b34fb "
1065 .to_string()
1066 ),
1067 Ok("00001800-0000-1000-8000-00805f9b34fb;00001801-0000-1000-8000-00805f9b34fb;"
1068 .to_string())
1069 );
1070 }
1071
1072 #[test]
test_replacesemicolonwithspace()1073 fn test_replacesemicolonwithspace() {
1074 // UUID example
1075 let mut key = DeviceKey::new("", KeyAction::Apply(Converter::ReplaceSemiColonWithSpace));
1076 assert_eq!(
1077 key.apply_action(
1078 "00001800-0000-1000-8000-00805f9b34fb;00001801-0000-1000-8000-00805f9b34fb;"
1079 .to_string()
1080 ),
1081 Ok("00001800-0000-1000-8000-00805f9b34fb 00001801-0000-1000-8000-00805f9b34fb "
1082 .to_string())
1083 );
1084 }
1085
1086 #[test]
test_irk_conversion()1087 fn test_irk_conversion() {
1088 let mut key = DeviceKey::new("", KeyAction::Apply(Converter::ReverseEndianUppercase));
1089 assert_eq!(
1090 key.apply_action("d584da72ceccfdf462405b558441ed44".to_string()),
1091 Ok("44ED4184555B4062F4FDCCCE72DA84D5".to_string())
1092 );
1093 assert_eq!(
1094 key.apply_action("td584da72ceccfdf462405b558441ed44".to_string()),
1095 Err("Error converting link key: invalid digit found in string".to_string())
1096 );
1097 }
1098
1099 #[test]
test_ltk_conversion()1100 fn test_ltk_conversion() {
1101 let floss_penc_key =
1102 String::from("48fdc93d776cd8cc918f31e422ece00d2322924fa9a09fb30eb20110");
1103 let pltk = LtkInfo::try_from_penc(floss_penc_key).unwrap_or_default();
1104 assert_eq!(pltk.key, 0x48FDC93D776CD8CC918F31E422ECE00D);
1105 assert_eq!(pltk.rand, 12943240503130989091);
1106 assert_eq!(pltk.ediv, 45582);
1107 assert_eq!(pltk.auth, 1);
1108 assert_eq!(pltk.len, 16);
1109 assert_eq!(
1110 LtkInfo::try_from_penc(
1111 "48fdc93d776cd8cc918f31e422ece00d2322924fa9a09fb30eb2011".to_string()
1112 )
1113 .unwrap_err(),
1114 "PENC String provided to LtkInfo is not the right size"
1115 );
1116
1117 let floss_lenc_key = String::from("07a104a6d2b464d74ff7e2e80b20295600001001");
1118 let lltk = LtkInfo::try_from_lenc(floss_lenc_key).unwrap_or_default();
1119 assert_eq!(lltk.key, 0x07A104A6D2B464D74FF7E2E80B202956);
1120 assert_eq!(lltk.rand, 0);
1121 assert_eq!(lltk.ediv, 0);
1122 assert_eq!(lltk.auth, 1);
1123 assert_eq!(lltk.len, 16);
1124 assert_eq!(
1125 LtkInfo::try_from_lenc("07a104a6d2b464d74ff7e2e80b2029560000100".to_string())
1126 .unwrap_err(),
1127 "LENC String provided to LtkInfo is not the right size"
1128 );
1129 }
1130
1131 #[test]
test_convert_from_bluez_device()1132 fn test_convert_from_bluez_device() {
1133 let test_addr = "00:11:22:33:44:55";
1134 let mut conf = Ini::new_cs();
1135 assert!(convert_from_bluez_device(
1136 "test/migrate/fake_bluez_info.toml",
1137 test_addr,
1138 &mut conf,
1139 false
1140 ));
1141 assert!(convert_from_bluez_device(
1142 "test/migrate/fake_bluez_hid.toml",
1143 test_addr,
1144 &mut conf,
1145 true
1146 ));
1147
1148 assert_eq!(conf.get(test_addr, "Name"), Some(String::from("Test Device")));
1149 assert_eq!(conf.get(test_addr, "DevClass"), Some(String::from("2360344")));
1150 assert_eq!(conf.get(test_addr, "Appearance"), Some(String::from("962")));
1151 assert_eq!(conf.get(test_addr, "DevType"), Some(String::from("1")));
1152 assert_eq!(
1153 conf.get(test_addr, "Service"),
1154 Some(String::from(
1155 "0000110b-0000-1000-8000-00805f9b34fb 0000110c-0000-1000-8000-00805f9b34fb "
1156 ))
1157 );
1158 assert_eq!(conf.get(test_addr, "AddrType"), Some(String::from("1")));
1159
1160 assert_eq!(
1161 conf.get(test_addr, "LinkKey"),
1162 Some(String::from("ffeeddccbbaa99887766554433221100"))
1163 );
1164 assert_eq!(conf.get(test_addr, "LinkKeyType"), Some(String::from("4")));
1165 assert_eq!(conf.get(test_addr, "PinLength"), Some(String::from("0")));
1166
1167 assert_eq!(conf.get(test_addr, "SdpDiVendorIdSource"), Some(String::from("1")));
1168 assert_eq!(conf.get(test_addr, "SdpDiManufacturer"), Some(String::from("100")));
1169 assert_eq!(conf.get(test_addr, "SdpDiModel"), Some(String::from("22222")));
1170 assert_eq!(conf.get(test_addr, "SdpDiHardwareVersion"), Some(String::from("3")));
1171
1172 assert_eq!(conf.get(test_addr, "VendorIdSource"), Some(String::from("1")));
1173 assert_eq!(conf.get(test_addr, "VendorId"), Some(String::from("100")));
1174 assert_eq!(conf.get(test_addr, "ProductId"), Some(String::from("22222")));
1175 assert_eq!(conf.get(test_addr, "ProductVersion"), Some(String::from("3")));
1176
1177 assert_eq!(
1178 conf.get(test_addr, "LE_KEY_PID"),
1179 Some(String::from("ffeeddccbbaa9988776655443322110001001122334455"))
1180 );
1181 assert_eq!(
1182 conf.get(test_addr, "LE_KEY_PENC"),
1183 Some(String::from("00112233445566778899aabbccddeeff8877665544332211bbaa0210"))
1184 );
1185
1186 assert_eq!(conf.get(test_addr, "HidAttrMask"), Some(String::from("0")));
1187 assert_eq!(
1188 conf.get(test_addr, "HidDescriptor"),
1189 Some(String::from("05010906a1018501050719e029e7150025017501950881029505050819012905910295017503910195067508150026a400050719002aa4008100c005010902a10185020901a1009510750115002501050919012910810205011601f826ff07750c95020930093181061581257f75089501093881069501050c0a38028106c0c0050c0901a1018503751095021501268c0219012a8c028160c00643ff0a0202a101851175089513150026ff000902810009029100c0"))
1190 );
1191 assert_eq!(conf.get(test_addr, "HidVersion"), Some(String::from("273")));
1192 assert_eq!(conf.get(test_addr, "HidCountryCode"), Some(String::from("3")));
1193 }
1194 }
1195