|
5 | 5 | use alloc::string::String; |
6 | 6 | use alloc::vec::Vec; |
7 | 7 |
|
| 8 | +use rand::random; |
| 9 | +use sha1::{Digest, Sha1}; |
8 | 10 | use std::fs::File; |
9 | 11 | use std::io::{self, BufRead, BufReader, Write}; |
10 | 12 | use std::path::Path; |
| 13 | +use std::thread; |
| 14 | +use std::time::Duration; |
11 | 15 |
|
12 | 16 | use crate::chromium_ec::i2c_passthrough::*; |
13 | 17 | use crate::chromium_ec::{CrosEc, EcError, EcResult}; |
@@ -160,6 +164,7 @@ enum SmartBatReg { |
160 | 164 | SerialNum = 0x1C, |
161 | 165 | ManufacturerName = 0x20, |
162 | 166 | DeviceName = 0x21, |
| 167 | + Authenticate = 0x2F, |
163 | 168 | CellVoltage1 = 0x3C, |
164 | 169 | CellVoltage2 = 0x3D, |
165 | 170 | CellVoltage3 = 0x3E, |
@@ -456,6 +461,33 @@ fn print_operation_status(value: u32) { |
456 | 461 | } |
457 | 462 | } |
458 | 463 |
|
| 464 | +/// Calculates the HMAC using TI's specific nested SHA-1 method. |
| 465 | +/// Formula: SHA1( Key || SHA1( Key || Challenge ) ) |
| 466 | +/// Note: TI batteries expect bytes in reversed order |
| 467 | +fn calculate_ti_hmac(key: &[u8; 16], challenge: &[u8; 20]) -> [u8; 20] { |
| 468 | + // Reverse challenge bytes as per TI spec |
| 469 | + let mut challenge_rev = *challenge; |
| 470 | + challenge_rev.reverse(); |
| 471 | + |
| 472 | + // 1. Inner Hash: SHA1( Key + Challenge_reversed ) |
| 473 | + let mut inner_hasher = Sha1::new(); |
| 474 | + inner_hasher.update(key); |
| 475 | + inner_hasher.update(challenge_rev); |
| 476 | + let inner_digest = inner_hasher.finalize(); |
| 477 | + |
| 478 | + // 2. Outer Hash: SHA1( Key + Inner_Digest ) |
| 479 | + let mut outer_hasher = Sha1::new(); |
| 480 | + outer_hasher.update(key); |
| 481 | + outer_hasher.update(inner_digest); |
| 482 | + let outer_digest = outer_hasher.finalize(); |
| 483 | + |
| 484 | + // Convert GenericArray to standard [u8; 20] and reverse for output |
| 485 | + let mut result = [0u8; 20]; |
| 486 | + result.copy_from_slice(&outer_digest); |
| 487 | + result.reverse(); |
| 488 | + result |
| 489 | +} |
| 490 | + |
459 | 491 | /// Reads a line from stdin without echoing (for sensitive input like keys) |
460 | 492 | #[cfg(unix)] |
461 | 493 | fn read_password() -> io::Result<String> { |
@@ -643,6 +675,136 @@ impl SmartBattery { |
643 | 675 | Ok(i2c_response.data[1..=actual_len].to_vec()) |
644 | 676 | } |
645 | 677 |
|
| 678 | + /// Raw I2C read without SMBus block length prefix handling |
| 679 | + fn read_raw(&self, ec: &CrosEc, addr: u16, len: u16) -> EcResult<Vec<u8>> { |
| 680 | + let i2c_response = i2c_read(ec, self.i2c_port, self.i2c_addr >> 1, addr, len)?; |
| 681 | + i2c_response.is_successful()?; |
| 682 | + Ok(i2c_response.data.to_vec()) |
| 683 | + } |
| 684 | + |
| 685 | + fn smbus_write_block(&self, ec: &CrosEc, reg: u8, data: &[u8]) -> EcResult<()> { |
| 686 | + i2c_write_block(ec, self.i2c_port, self.i2c_addr >> 1, reg, data)?; |
| 687 | + Ok(()) |
| 688 | + } |
| 689 | + |
| 690 | + /// Authenticate the battery using SHA-1 HMAC challenge-response |
| 691 | + pub fn authenticate_battery(&self, ec: &CrosEc, auth_key: &[u8; 16]) -> EcResult<bool> { |
| 692 | + // 1. Generate a random 20-byte challenge |
| 693 | + let challenge: [u8; 20] = random(); |
| 694 | + |
| 695 | + println!("Step 1: Sending Challenge..."); |
| 696 | + |
| 697 | + // SMBus Block Write format: [Byte_Count, Data...] |
| 698 | + // The register address is handled by smbus_write_block |
| 699 | + let mut write_buf = Vec::new(); |
| 700 | + write_buf.push(20); // Byte Count (0x14) |
| 701 | + write_buf.extend_from_slice(&challenge); |
| 702 | + |
| 703 | + self.smbus_write_block(ec, SmartBatReg::Authenticate as u8, &write_buf)?; |
| 704 | + |
| 705 | + // 2. Wait for the gauge to calculate (Datasheet says 250ms) |
| 706 | + println!("Step 2: Waiting 250ms..."); |
| 707 | + thread::sleep(Duration::from_millis(250)); |
| 708 | + |
| 709 | + // 3. Calculate expected result locally while waiting |
| 710 | + let expected_response = calculate_ti_hmac(auth_key, &challenge); |
| 711 | + |
| 712 | + // 4. Read Response from Authenticate register |
| 713 | + // SMBus Block Read format: [Length] + [Data...] |
| 714 | + println!("Step 3: Reading Response..."); |
| 715 | + |
| 716 | + // Read 21 bytes (1 byte length + 20 bytes signature) from Authenticate register |
| 717 | + let raw_response = self.read_raw(ec, SmartBatReg::Authenticate as u16, 21)?; |
| 718 | + |
| 719 | + // 5. Parse and Compare |
| 720 | + // First byte is the length, should be 20 |
| 721 | + let response_len = raw_response[0] as usize; |
| 722 | + if response_len != 20 { |
| 723 | + return Err(EcError::DeviceError(format!( |
| 724 | + "Expected 20-byte response, got {} bytes (first byte: 0x{:02X})", |
| 725 | + response_len, raw_response[0] |
| 726 | + ))); |
| 727 | + } |
| 728 | + |
| 729 | + let device_response = &raw_response[1..21]; |
| 730 | + |
| 731 | + println!("Expected: {:02X?}", expected_response); |
| 732 | + println!("Received: {:02X?}", device_response); |
| 733 | + |
| 734 | + if device_response == expected_response { |
| 735 | + println!("SUCCESS: Battery is genuine."); |
| 736 | + Ok(true) |
| 737 | + } else { |
| 738 | + println!("FAILURE: Signature mismatch."); |
| 739 | + Ok(false) |
| 740 | + } |
| 741 | + } |
| 742 | + |
| 743 | + /// Interactive authentication - prompts for unseal key and authentication key |
| 744 | + pub fn interactive_authenticate(&self, ec: &CrosEc) -> EcResult<()> { |
| 745 | + // First, try to unseal the battery |
| 746 | + println!("Some batteries require unsealing before authentication."); |
| 747 | + print!("Unseal key in hex (e.g. 04143672), or enter to skip: "); |
| 748 | + io::stdout() |
| 749 | + .flush() |
| 750 | + .map_err(|e| EcError::DeviceError(format!("Failed to flush stdout: {}", e)))?; |
| 751 | + |
| 752 | + let unseal_input = read_password() |
| 753 | + .map_err(|e| EcError::DeviceError(format!("Failed to read key: {}", e)))?; |
| 754 | + let unseal_input = unseal_input.trim(); |
| 755 | + if !unseal_input.is_empty() { |
| 756 | + let key: u32 = u32::from_str_radix(unseal_input, 16) |
| 757 | + .map_err(|e| EcError::DeviceError(format!("Invalid unseal key: {}", e)))?; |
| 758 | + println!("Unsealing battery..."); |
| 759 | + self.unseal(ec, (key >> 16) as u16, key as u16)?; |
| 760 | + // Wait a bit after unsealing |
| 761 | + thread::sleep(Duration::from_millis(100)); |
| 762 | + } |
| 763 | + |
| 764 | + // Now prompt for authentication key |
| 765 | + print!("Auth key in hex (32 chars, e.g. 00112233...EEFF): "); |
| 766 | + io::stdout() |
| 767 | + .flush() |
| 768 | + .map_err(|e| EcError::DeviceError(format!("Failed to flush stdout: {}", e)))?; |
| 769 | + |
| 770 | + let input_text = read_password() |
| 771 | + .map_err(|e| EcError::DeviceError(format!("Failed to read key: {}", e)))?; |
| 772 | + let input_text = input_text.trim(); |
| 773 | + if input_text.is_empty() { |
| 774 | + println!("No key provided, aborting."); |
| 775 | + return Ok(()); |
| 776 | + } |
| 777 | + |
| 778 | + if input_text.len() != 32 { |
| 779 | + return Err(EcError::DeviceError(format!( |
| 780 | + "Key must be 32 hex characters (16 bytes), got {} characters", |
| 781 | + input_text.len() |
| 782 | + ))); |
| 783 | + } |
| 784 | + |
| 785 | + let mut auth_key = [0u8; 16]; |
| 786 | + for i in 0..16 { |
| 787 | + auth_key[i] = u8::from_str_radix(&input_text[i * 2..i * 2 + 2], 16).map_err(|e| { |
| 788 | + EcError::DeviceError(format!("Invalid hex at position {}: {}", i * 2, e)) |
| 789 | + })?; |
| 790 | + } |
| 791 | + |
| 792 | + let result = self.authenticate_battery(ec, &auth_key)?; |
| 793 | + |
| 794 | + // Re-seal the battery if we unsealed it |
| 795 | + if !unseal_input.is_empty() { |
| 796 | + println!("Re-sealing battery..."); |
| 797 | + self.seal(ec)?; |
| 798 | + } |
| 799 | + |
| 800 | + match result { |
| 801 | + true => println!("Authentication successful - battery is genuine."), |
| 802 | + false => println!("Authentication failed - battery signature mismatch."), |
| 803 | + } |
| 804 | + |
| 805 | + Ok(()) |
| 806 | + } |
| 807 | + |
646 | 808 | /// Print battery information interactively (prompts for unseal key) |
647 | 809 | pub fn dump_data(&self, ec: &CrosEc) -> EcResult<()> { |
648 | 810 | // Prompt for unseal key to access ManufacturerAccess data |
|
0 commit comments