| // Copyright 2022 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use std::io; |
| use std::io::Write; |
| use std::path::Path; |
| |
| use crate::command_runner::CommandRunner; |
| use crate::context::Context; |
| use crate::error::HwsecError; |
| use crate::gsc::clear_terminal; |
| use crate::gsc::run_gsctool_cmd; |
| |
| // RMA Reset Authorization parameters. |
| // - URL of Reset Authorization Server. |
| const RMA_SERVER: &str = "https://www.google.com/chromeos/partner/console/cr50reset"; |
| // - Number of retries before giving up. |
| const MAX_RETRIES: i32 = 3; |
| // - RETRY_DELAY=10 |
| const RETRY_DELAY: i32 = 10; |
| const FRECON_PID_FILE: &str = "/run/frecon/pid"; |
| |
| fn gbb_force_dev_mode(ctx: &mut impl Context) -> Result<(), HwsecError> { |
| // Disable SW WP and set GBB_FLAG_FORCE_DEV_SWITCH_ON (0x8) to force boot in |
| // developer mode after RMA reset. |
| |
| ctx.cmd_runner() |
| .run("futility", vec!["gbb", "--flash", "--set", "--flags=+0x8"]) |
| .map_err(|_| { |
| eprintln!("Failed to run futility"); |
| HwsecError::CommandRunnerError |
| })?; |
| Ok(()) |
| } |
| |
| fn get_crossystem_hwid(ctx: &mut impl Context) -> Result<String, HwsecError> { |
| // Get HWID and replace whitespace with underscore. |
| Ok(ctx |
| .cmd_runner() |
| .output("crossystem", vec!["hwid"]) |
| .map_err(|_| { |
| eprintln!("Failed to get hwid."); |
| HwsecError::CommandRunnerError |
| })? |
| .replace(' ', "_")) |
| } |
| |
| /// Retrieve the challenge to perform Cr50 reset from 'gsctool -tr', which has raw response of the |
| /// following format: |
| /// |
| /// Challenge: |
| /// AEDNM 6GCYN C7Q55 5HYS7 3SECR KRQRL ERXG7 HFSNF |
| /// CAZDM XWTDR HAWDE 36GWE UDMKP H7TSM RRTV5 CWS75 |
| fn get_challenge_string_from_gsctool(ctx: &mut impl Context) -> Result<String, HwsecError> { |
| let gsctool_output = |
| run_gsctool_cmd(ctx, vec!["--trunks_send", "--rma_auth"]).map_err(|e| { |
| eprintln!("Failed to run gsctool."); |
| e |
| })?; |
| |
| if !gsctool_output.status.success() { |
| eprintln!("{}", std::str::from_utf8(&gsctool_output.stderr).unwrap()); |
| return Err(HwsecError::GsctoolError( |
| gsctool_output.status.code().unwrap(), |
| )); |
| } |
| |
| let challenge_string = std::str::from_utf8(&gsctool_output.stdout) |
| .map_err(|_| { |
| eprintln!("Internal error occurred."); |
| HwsecError::GsctoolResponseBadFormatError |
| })? |
| .replace("Challenge:", ""); |
| |
| // Test if we have a challenge. |
| if challenge_string.is_empty() { |
| return Err(HwsecError::GsctoolResponseBadFormatError); |
| } |
| |
| // result may contain whitespace and newline characters |
| Ok(challenge_string) |
| } |
| |
| /// This function returns the challenge url string, and prints output similarly as follows |
| /// in the terminal: |
| /// |
| /// Challenge: |
| /// |
| /// AEDNM 6GCYN C7Q55 5HYS7 3SECR KRQRL ERXG7 HFSNF |
| /// CAZDM XWTDR HAWDE 36GWE UDMKP H7TSM RRTV5 CWS75 |
| /// |
| /// URL: https://www.google.com/chromeos/partner/console/cr50reset?\ |
| /// challenge=AEDNM6GCYNC7Q555HYS73SECRKRQRLERXG7HFSNFCAZDMXWTDRHAWDE36GWEUDMKPH7TSMRRTV5CWS75\ |
| /// &hwid=VOLET_TEST_5042 |
| fn generate_challenge_url_and_display_challenge( |
| ctx: &mut impl Context, |
| ) -> Result<String, HwsecError> { |
| // Get HWID and replace whitespace with underscore. |
| let hwid = get_crossystem_hwid(ctx)?; |
| // Get challenge string and remove "Challenge:". |
| let challenge_string = get_challenge_string_from_gsctool(ctx).map_err(|_| { |
| eprintln!("Challenge wasn't generated. CR50 might need updating."); |
| HwsecError::InternalError |
| })?; |
| |
| // Preserve enough space to prevent terminal scrolling. |
| clear_terminal(); |
| |
| // Display the challenge. |
| println!("Challenge:"); |
| println!("{}", challenge_string); |
| |
| // Remove whitespace and newline from challenge. |
| let challenge_string = challenge_string.replace(['\n', ' '], ""); |
| |
| // Calculate challenge URL and display it. |
| let challenge_url = format!( |
| "{}?challenge={}&hwid={}", |
| RMA_SERVER, challenge_string, hwid |
| ); |
| println!("URL: {}", challenge_url); |
| Ok(challenge_url) |
| } |
| |
| pub fn gsc_reset(ctx: &mut impl Context) -> Result<(), HwsecError> { |
| const WAIT_TO_ENTER_RMA_SECS: u64 = 2; |
| const SECS_IN_A_DAY: u64 = 86400; |
| |
| // Make sure frecon is running. |
| let frecon_pid = ctx.read_file_to_string(FRECON_PID_FILE)?; |
| |
| // This is the path to the pre-chroot filesystem. Since frecon is started |
| // before the chroot, all files that frecon accesses must be copied to |
| // this path. |
| let chg_str_path = format!("/proc/{}/root", frecon_pid); |
| |
| if !Path::new(&chg_str_path).exists() { |
| eprintln!("frecon not running. Can't display qrcode."); |
| return Err(HwsecError::FileError); |
| } |
| let challenge_url = generate_challenge_url_and_display_challenge(ctx)?; |
| |
| // Create qrcode and display it. |
| // TODO: replace qrencode command with qrcode library like this |
| // |
| // let qrcode = QrCode::new(challenge_string).unwrap(); |
| // let image = qrcode.render::<Luma<u8>>().build(); |
| // image.save(format!("{chg_str_path}/chg.png")).unwrap(); |
| ctx.cmd_runner() |
| .run( |
| "qrencode", |
| vec![ |
| "-s", |
| "5", |
| "-o", |
| &format!("{}/chg.png", chg_str_path), |
| &challenge_url, |
| ], |
| ) |
| .map_err(|_| { |
| eprintln!("Failed to qrencode."); |
| HwsecError::QrencodeError |
| })?; |
| ctx.write_contents_to_file("/run/frecon/vt0", b"\x1b]image:file=/chg.png\x1b\\")?; |
| for _ in 0..MAX_RETRIES { |
| // Read authorization code. Show input in uppercase letters. |
| print!("\nEnter authorization code: "); |
| // Flush stdout buffer. Here we ignore possible i/o error. |
| io::stdout().flush().ok(); |
| let mut auth_code = String::new(); |
| while io::stdin().read_line(&mut auth_code).is_err() { |
| println!("Please only enter ASCII characters."); |
| } |
| let auth_code = auth_code.trim().to_uppercase(); |
| |
| // Test authorization code. |
| let gsctool_output = run_gsctool_cmd(ctx, vec!["--trunks_send", "--rma_auth", &auth_code]) |
| .map_err(|e| { |
| eprintln!("Failed to run gsctool."); |
| e |
| })?; |
| |
| if gsctool_output.status.success() { |
| println!("The system will reboot shortly."); |
| // Wait for cr50 to enter RMA mode. |
| ctx.sleep(WAIT_TO_ENTER_RMA_SECS); |
| |
| // Force the next boot to be in developer mode so that we can boot to |
| // RMA shim again. |
| gbb_force_dev_mode(ctx).map_err(|e| { |
| eprintln!("gbb_force_dev_mode failed."); |
| e |
| })?; |
| |
| // TODO: reboot with function call instead |
| ctx.cmd_runner() |
| .run("reboot", Vec::<&str>::new()) |
| .map_err(|_| { |
| eprintln!("Failed to reboot."); |
| HwsecError::SystemRebootError |
| })?; |
| |
| // Sleep indefinitely to avoid continue. |
| ctx.sleep(SECS_IN_A_DAY); |
| } else { |
| eprintln!("{}", std::str::from_utf8(&gsctool_output.stderr).unwrap()); |
| } |
| |
| println!("Invalid authorization code. Please try again.\n"); |
| } |
| |
| println!("Number of retries exceeded. Another qrcode will generate in 10s."); |
| |
| for _ in 0..RETRY_DELAY { |
| print!("."); |
| // Flush stdout buffer. Here we ignore possible i/o error. |
| io::stdout().flush().ok(); |
| ctx.sleep(1); |
| } |
| |
| Ok(()) |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use super::generate_challenge_url_and_display_challenge; |
| use super::get_challenge_string_from_gsctool; |
| use super::get_crossystem_hwid; |
| use crate::command_runner::MockCommandInput; |
| use crate::command_runner::MockCommandOutput; |
| use crate::context::mock::MockContext; |
| use crate::context::Context; |
| use crate::error::HwsecError; |
| |
| #[test] |
| fn test_get_crossystem_hwid() { |
| let mut mock_ctx = MockContext::new(); |
| mock_ctx.cmd_runner().add_expectation( |
| MockCommandInput::new("crossystem", vec!["hwid"]), |
| MockCommandOutput::new(0, "VOLET TEST 5042", ""), |
| ); |
| let result = get_crossystem_hwid(&mut mock_ctx); |
| assert_eq!(result, Ok(String::from("VOLET_TEST_5042"))); |
| } |
| |
| #[test] |
| fn test_get_challenge_string_from_gsctool_ok() { |
| let mut mock_ctx = MockContext::new(); |
| mock_ctx.cmd_runner().add_gsctool_interaction( |
| vec!["--trunks_send", "--rma_auth"], |
| 0, |
| "Challenge:\nMOCK CHALLENGE\n", |
| "", |
| ); |
| let result = get_challenge_string_from_gsctool(&mut mock_ctx); |
| assert_eq!(result, Ok(String::from("\nMOCK CHALLENGE\n"))); |
| } |
| |
| #[test] |
| fn test_get_challenge_string_from_gsctool_failed_attempt() { |
| let mut mock_ctx = MockContext::new(); |
| mock_ctx.cmd_runner().add_gsctool_interaction( |
| vec!["--trunks_send", "--rma_auth"], |
| 3, |
| "", |
| "error 4", |
| ); |
| let result = get_challenge_string_from_gsctool(&mut mock_ctx); |
| assert_eq!(result, Err(HwsecError::GsctoolError(3))); |
| } |
| #[test] |
| fn test_get_challenge_string_from_gsctool_empty_challenge() { |
| let mut mock_ctx = MockContext::new(); |
| mock_ctx.cmd_runner().add_gsctool_interaction( |
| vec!["--trunks_send", "--rma_auth"], |
| 0, |
| "Challenge:", |
| "", |
| ); |
| let result = get_challenge_string_from_gsctool(&mut mock_ctx); |
| assert_eq!(result, Err(HwsecError::GsctoolResponseBadFormatError)); |
| } |
| |
| // The follow test input/expected output is from a real result generated by running |
| // gsc_reset on DUT |
| // |
| // Challenge: |
| // |
| // AEDNM 6GCYN C7Q55 5HYS7 3SECR KRQRL ERXG7 HFSNF |
| // CAZDM XWTDR HAWDE 36GWE UDMKP H7TSM RRTV5 CWS75 |
| // |
| // URL: https://www.google.com/chromeos/partner/console/cr50reset?\ |
| // challenge=AEDNM6GCYNC7Q555HYS73SECRKRQRLERXG7HFSNFCAZDMXWTDRHAWDE36GWEUDMKPH7TSMRRTV5CWS75\ |
| // &hwid=VOLET_TEST_5042 |
| // |
| // Enter authorization code: |
| #[test] |
| fn test_generate_challenge_url_and_display_challenge_ok() { |
| let mut mock_ctx = MockContext::new(); |
| mock_ctx.cmd_runner().add_expectation( |
| MockCommandInput::new("crossystem", vec!["hwid"]), |
| MockCommandOutput::new(0, "VOLET TEST 5042", ""), |
| ); |
| mock_ctx.cmd_runner().add_gsctool_interaction( |
| vec!["--trunks_send", "--rma_auth"], |
| 0, |
| include_str!( |
| "../command_runner/expected_message/successfully_gsctool_rma_auth_response.txt" |
| ), |
| "", |
| ); |
| let result = generate_challenge_url_and_display_challenge(&mut mock_ctx); |
| let expected_url = "https://www.google.com/chromeos/partner/console/cr50reset?challenge=\ |
| AEDNM6GCYNC7Q555HYS73SECRKRQRLERXG7HFSNFCAZDMXWTDRHAWDE36GWEUDMKPH7TSMRRTV5CWS75&\ |
| hwid=VOLET_TEST_5042"; |
| assert_eq!(result, Ok(String::from(expected_url))); |
| } |
| |
| #[test] |
| fn test_generate_challenge_url_and_display_challenge_fail_challenge_not_generated() { |
| let mut mock_ctx = MockContext::new(); |
| mock_ctx.cmd_runner().add_expectation( |
| MockCommandInput::new("crossystem", vec!["hwid"]), |
| MockCommandOutput::new(0, "MOCK HWID", ""), |
| ); |
| mock_ctx.cmd_runner().add_gsctool_interaction( |
| vec!["--trunks_send", "--rma_auth"], |
| 3, |
| "", |
| "error 4", |
| ); |
| let result = generate_challenge_url_and_display_challenge(&mut mock_ctx); |
| assert_eq!(result, Err(HwsecError::InternalError)); |
| } |
| } |