| // 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)); | |
| } | |
| } |