Skip to content

Commit 60dad5f

Browse files
committed
smart_battery: Add SHA-1 HMAC battery authentication
Add battery authentication using TI's nested SHA-1 HMAC method: - calculate_ti_hmac(): Implements SHA1(Key || SHA1(Key || Challenge)) - authenticate_battery(): Challenge-response authentication flow - interactive_authenticate(): CLI interface with unseal/auth key prompts The authentication verifies battery authenticity by: 1. Sending a random 20-byte challenge to the battery 2. Waiting 250ms for the gauge to compute the response 3. Computing expected response locally using the auth key 4. Comparing responses to verify genuineness Add --smartbattery-auth CLI flag for interactive authentication. Requires sha1 and rand crate dependencies. Signed-off-by: Daniel Schaefer <dhs@frame.work>
1 parent b11ea77 commit 60dad5f

File tree

6 files changed

+250
-0
lines changed

6 files changed

+250
-0
lines changed

Cargo.lock

Lines changed: 71 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

framework_lib/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ built = { version = "0.8", features = ["chrono", "git2"] }
2424

2525
[dependencies]
2626
lazy_static = "1.4.0"
27+
sha1 = { version = "0.10.6", default-features = false }
2728
sha2 = { version = "0.10.8", default-features = false, features = [ "force-soft" ] }
2829
regex = { version = "1.11.1", default-features = false }
2930
num = { version = "0.4", default-features = false }
@@ -44,6 +45,7 @@ redox_hwio = { git = "https://github.com/FrameworkComputer/rust-hwio", branch =
4445
smbios-lib = { git = "https://github.com/FrameworkComputer/smbios-lib.git", branch = "no-std", default-features = false }
4546

4647
[target.'cfg(windows)'.dependencies]
48+
rand = { version = "0.9", default-features = false, features = ["std", "std_rng", "thread_rng"] }
4749
wmi = "0.18"
4850
smbios-lib = { git = "https://github.com/FrameworkComputer/smbios-lib.git", branch = "no-std" }
4951
env_logger = "0.11"
@@ -55,6 +57,7 @@ winreg = "0.55.0"
5557
nvml-wrapper = { version = "0.11.0", optional = true }
5658

5759
[target.'cfg(unix)'.dependencies]
60+
rand = { version = "0.9", default-features = false, features = ["std", "std_rng", "thread_rng"] }
5861
libc = "0.2.155"
5962
nix = { version = "0.30", features = ["ioctl", "user", "term"] }
6063
redox_hwio = { git = "https://github.com/FrameworkComputer/rust-hwio", branch = "freebsd" }
@@ -80,6 +83,7 @@ features = [
8083
"Win32_Devices_Properties",
8184
"Win32_Storage_EnhancedStorage",
8285
"Win32_System_Threading",
86+
"Win32_System_Console",
8387
"Win32_UI_Shell_PropertiesSystem"
8488
]
8589

framework_lib/src/commandline/clap_std.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,10 @@ struct ClapCli {
5555
#[arg(long, value_name = "FILE")]
5656
smartbattery: Option<Option<PathBuf>>,
5757

58+
/// Authenticate smart battery (requires unseal and auth keys)
59+
#[arg(long)]
60+
smartbattery_auth: bool,
61+
5862
/// Print thermal information (Temperatures and Fan speed)
5963
#[arg(long)]
6064
thermal: bool,
@@ -416,6 +420,7 @@ pub fn parse(args: &[String]) -> Cli {
416420
smartbattery: args
417421
.smartbattery
418422
.map(|opt| opt.map(|x| x.into_os_string().into_string().unwrap())),
423+
smartbattery_auth: args.smartbattery_auth,
419424
thermal: args.thermal,
420425
sensors: args.sensors,
421426
fansetduty,

framework_lib/src/commandline/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,7 @@ pub struct Cli {
170170
pub compare_version: Option<String>,
171171
pub power: bool,
172172
pub smartbattery: Option<Option<String>>,
173+
pub smartbattery_auth: bool,
173174
pub thermal: bool,
174175
pub sensors: bool,
175176
pub fansetduty: Option<(Option<u32>, u32)>,
@@ -1522,6 +1523,12 @@ pub fn run_with_args(args: &Cli, _allupdate: bool) -> i32 {
15221523
}
15231524
}
15241525
}
1526+
} else if args.smartbattery_auth {
1527+
#[cfg(not(feature = "uefi"))]
1528+
{
1529+
let bat = SmartBattery::new();
1530+
print_err(bat.interactive_authenticate(&ec));
1531+
}
15251532
} else if args.thermal {
15261533
power::print_thermal(&ec);
15271534
} else if args.sensors {

framework_lib/src/commandline/uefi.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ pub fn parse(args: &[String]) -> Cli {
3636
compare_version: None,
3737
power: false,
3838
smartbattery: None,
39+
smartbattery_auth: false,
3940
thermal: false,
4041
sensors: false,
4142
fansetduty: None,

framework_lib/src/smart_battery.rs

Lines changed: 162 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
use alloc::string::String;
66
use alloc::vec::Vec;
77

8+
use rand::random;
9+
use sha1::{Digest, Sha1};
810
use std::fs::File;
911
use std::io::{self, BufRead, BufReader, Write};
1012
use std::path::Path;
13+
use std::thread;
14+
use std::time::Duration;
1115

1216
use crate::chromium_ec::i2c_passthrough::*;
1317
use crate::chromium_ec::{CrosEc, EcError, EcResult};
@@ -160,6 +164,7 @@ enum SmartBatReg {
160164
SerialNum = 0x1C,
161165
ManufacturerName = 0x20,
162166
DeviceName = 0x21,
167+
Authenticate = 0x2F,
163168
CellVoltage1 = 0x3C,
164169
CellVoltage2 = 0x3D,
165170
CellVoltage3 = 0x3E,
@@ -456,6 +461,33 @@ fn print_operation_status(value: u32) {
456461
}
457462
}
458463

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+
459491
/// Reads a line from stdin without echoing (for sensitive input like keys)
460492
#[cfg(unix)]
461493
fn read_password() -> io::Result<String> {
@@ -643,6 +675,136 @@ impl SmartBattery {
643675
Ok(i2c_response.data[1..=actual_len].to_vec())
644676
}
645677

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+
646808
/// Print battery information interactively (prompts for unseal key)
647809
pub fn dump_data(&self, ec: &CrosEc) -> EcResult<()> {
648810
// Prompt for unseal key to access ManufacturerAccess data

0 commit comments

Comments
 (0)