update_engine: Support recovery key version

Parse the recovery key version and send within the request to Omaha
server. This is to distinguish between various recovery keys a device
might be using.

When a recovery key can't be read or is unparsable, the server will be
receiving an empty attribute like: recoverykeyversion=""
Otherwise, a positive integer should be parsed and set within the
`recoverykeyversion` attribute.

It is up to the server to decide what to do when the
`recoverykeyversion` field is missing and make a decision based off
additional fields in the request or simply not serve an update for the
apps that utilize this attribute.

```
INFO update_engine: [omaha_request_action.cc(273)] Request:
<?xml version="1.0" encoding="UTF-8"?>
<request requestid="6c5e5d4b-d3ca-44bc-8f33-ef1d7ed66879"
  sessionid="35891a80-d3c3-46c8-9721-c9a9f525360b" protocol="3.0"
  updater="ChromeOSUpdateEngine" updaterversion="0.1.0.0"
  installsource="ondemandupdate" ismachine="1" recoverykeyversion="1" >
...
</request>
```

BUG=b:267545513
TEST=emerge
TEST=deploy

Change-Id: I9344626c4e505af070a5e5b896652c29e5c0dd7f
Reviewed-on: https://chromium-review.googlesource.com/c/aosp/platform/system/update_engine/+/4222216
Tested-by: Jae Hoon Kim <kimjae@chromium.org>
Commit-Queue: Jae Hoon Kim <kimjae@chromium.org>
Reviewed-by: Julius Werner <jwerner@chromium.org>
Reviewed-by: Yuanpeng Ni‎ <yuanpengni@chromium.org>
diff --git a/common/fake_hardware.h b/common/fake_hardware.h
index f883217..a56eb88 100644
--- a/common/fake_hardware.h
+++ b/common/fake_hardware.h
@@ -145,6 +145,14 @@
     return false;
   }
 
+  bool GetRecoveryKeyVersion(std::string* version) override {
+    if (recovery_key_version_.empty()) {
+      return false;
+    }
+    *version = recovery_key_version_;
+    return true;
+  }
+
   bool GetPowerwashSafeDirectory(base::FilePath* path) const override {
     return false;
   }
@@ -230,6 +238,10 @@
 
   void SetWarmReset(bool warm_reset) override { warm_reset_ = warm_reset; }
 
+  void SetRecoveryKeyVersion(const std::string& version) {
+    recovery_key_version_ = version;
+  }
+
   // Getters to verify state.
   int GetMaxKernelKeyRollforward() const { return kernel_max_rollforward_; }
 
@@ -273,6 +285,7 @@
   int64_t build_timestamp_{0};
   bool first_active_omaha_ping_sent_{false};
   bool warm_reset_{false};
+  std::string recovery_key_version_;
   mutable std::map<std::string, std::string> partition_timestamps_;
 };
 
diff --git a/common/hardware_interface.h b/common/hardware_interface.h
index 89c9b01..b54434d 100644
--- a/common/hardware_interface.h
+++ b/common/hardware_interface.h
@@ -125,6 +125,10 @@
   // directory available, returns false.
   virtual bool GetNonVolatileDirectory(base::FilePath* path) const = 0;
 
+  // Returns the recovery key version that the device is using.
+  // If key is not found or invalid, returns empty string.
+  virtual bool GetRecoveryKeyVersion(std::string* version) = 0;
+
   // Store in |path| the path to a non-volatile directory persisted across
   // powerwash cycles. In case of an error, such as no directory available,
   // returns false.
diff --git a/common/platform_constants.h b/common/platform_constants.h
index a958e93..1caf659 100644
--- a/common/platform_constants.h
+++ b/common/platform_constants.h
@@ -50,6 +50,9 @@
 // The stateful directory used by update_engine.
 extern const char kNonVolatileDirectory[];
 
+// Recovery key version file that exists under the non-volatile directory.
+extern const char kRecoveryKeyVersionFileName[];
+
 // Options passed to the filesystem when mounting the new partition during
 // postinstall.
 extern const char kPostinstallMountOptions[];
diff --git a/cros/hardware_chromeos.cc b/cros/hardware_chromeos.cc
index 3e52808..f5d127d 100644
--- a/cros/hardware_chromeos.cc
+++ b/cros/hardware_chromeos.cc
@@ -37,6 +37,7 @@
 #include "update_engine/common/hwid_override.h"
 #include "update_engine/common/platform_constants.h"
 #include "update_engine/common/subprocess.h"
+#include "update_engine/common/system_state.h"
 #include "update_engine/common/utils.h"
 #include "update_engine/cros/dbus_connection.h"
 #if USE_CFM || USE_REPORT_REQUISITION
@@ -110,6 +111,9 @@
 
 }  // namespace hardware
 
+HardwareChromeOS::HardwareChromeOS()
+    : root_("/"), non_volatile_path_(constants::kNonVolatileDirectory) {}
+
 void HardwareChromeOS::Init() {
   LoadConfig("" /* root_prefix */, IsNormalBootMode());
   debugd_proxy_.reset(
@@ -307,7 +311,48 @@
 }
 
 bool HardwareChromeOS::GetNonVolatileDirectory(base::FilePath* path) const {
-  *path = base::FilePath(constants::kNonVolatileDirectory);
+  *path = non_volatile_path_;
+  return true;
+}
+
+bool HardwareChromeOS::GetRecoveryKeyVersion(std::string* version) {
+  // Returned the cached value to read once per boot if read successfully.
+  if (!recovery_key_version_.empty()) {
+    *version = recovery_key_version_;
+    return true;
+  }
+
+  // Clear for safety.
+  version->clear();
+
+  base::FilePath non_volatile_path;
+  if (!GetNonVolatileDirectory(&non_volatile_path)) {
+    LOG(ERROR) << "Failed to get non-volatile path.";
+    return false;
+  }
+  auto recovery_key_version_path =
+      non_volatile_path.Append(constants::kRecoveryKeyVersionFileName);
+
+  // Use temporary version string to return empty string on read failure.
+  string tmp_version;
+  if (!base::ReadFileToString(recovery_key_version_path, &tmp_version)) {
+    LOG(ERROR) << "Failed to read recovery key version file at: "
+               << recovery_key_version_path.value();
+    return false;
+  }
+  base::TrimWhitespaceASCII(tmp_version, base::TRIM_ALL, &tmp_version);
+
+  // Check that the version is a valid string of integer.
+  int x;
+  if (!base::StringToInt(tmp_version, &x)) {
+    LOG(ERROR) << "Recovery key version file does not hold a valid version: "
+               << tmp_version;
+    return false;
+  }
+
+  // Only perfect conversions above return true, so safe to return the string
+  // itself without using `NumberToString(...)` or alike.
+  *version = tmp_version;
   return true;
 }
 
diff --git a/cros/hardware_chromeos.h b/cros/hardware_chromeos.h
index 089fe0d..339dce3 100644
--- a/cros/hardware_chromeos.h
+++ b/cros/hardware_chromeos.h
@@ -33,7 +33,7 @@
 // process.
 class HardwareChromeOS final : public HardwareInterface {
  public:
-  HardwareChromeOS() : root_("/") {}
+  HardwareChromeOS();
   HardwareChromeOS(const HardwareChromeOS&) = delete;
   HardwareChromeOS& operator=(const HardwareChromeOS&) = delete;
 
@@ -62,6 +62,7 @@
   bool SchedulePowerwash(bool save_rollback_data) override;
   bool CancelPowerwash() override;
   bool GetNonVolatileDirectory(base::FilePath* path) const override;
+  bool GetRecoveryKeyVersion(std::string* version) override;
   bool GetPowerwashSafeDirectory(base::FilePath* path) const override;
   int64_t GetBuildTimestamp() const override;
   bool AllowDowngrade() const override { return false; }
@@ -77,6 +78,9 @@
       const std::string& new_version) const override;
 
   void SetRootForTest(base::FilePath test_root) { root_ = test_root; }
+  void SetNonVolatileDirectoryForTest(const base::FilePath& path) {
+    non_volatile_path_ = path;
+  }
 
  private:
   friend class HardwareChromeOSTest;
@@ -88,7 +92,10 @@
 
   bool is_oobe_enabled_;
 
+  std::string recovery_key_version_;
+
   base::FilePath root_;
+  base::FilePath non_volatile_path_;
 
   std::unique_ptr<org::chromium::debugdProxyInterface> debugd_proxy_;
 };
diff --git a/cros/hardware_chromeos_unittest.cc b/cros/hardware_chromeos_unittest.cc
index 83425bd..96797e8 100644
--- a/cros/hardware_chromeos_unittest.cc
+++ b/cros/hardware_chromeos_unittest.cc
@@ -26,6 +26,7 @@
 
 #include "update_engine/common/constants.h"
 #include "update_engine/common/fake_hardware.h"
+#include "update_engine/common/platform_constants.h"
 #include "update_engine/common/test_utils.h"
 #include "update_engine/update_manager/umtest_utils.h"
 
@@ -193,4 +194,66 @@
   EXPECT_FALSE(hardware_.IsRunningFromMiniOs());
 }
 
+TEST_F(HardwareChromeOSTest, RecoveryKeyVersionMissingFile) {
+  base::FilePath test_path = root_dir_.GetPath();
+  hardware_.SetNonVolatileDirectoryForTest(test_path);
+
+  base::FilePath non_volatile_directory;
+  ASSERT_TRUE(hardware_.GetNonVolatileDirectory(&non_volatile_directory));
+  ASSERT_TRUE(base::CreateDirectory(non_volatile_directory));
+
+  std::string version;
+  EXPECT_FALSE(hardware_.GetRecoveryKeyVersion(&version));
+}
+
+TEST_F(HardwareChromeOSTest, RecoveryKeyVersionBadKey) {
+  base::FilePath test_path = root_dir_.GetPath();
+  hardware_.SetNonVolatileDirectoryForTest(test_path);
+
+  base::FilePath non_volatile_directory;
+  ASSERT_TRUE(hardware_.GetNonVolatileDirectory(&non_volatile_directory));
+  ASSERT_TRUE(base::CreateDirectory(non_volatile_directory));
+
+  EXPECT_TRUE(base::WriteFile(
+      non_volatile_directory.Append(constants::kRecoveryKeyVersionFileName),
+      "foobar"));
+
+  std::string version;
+  EXPECT_FALSE(hardware_.GetRecoveryKeyVersion(&version));
+}
+
+TEST_F(HardwareChromeOSTest, RecoveryKeyVersion) {
+  base::FilePath test_path = root_dir_.GetPath();
+  hardware_.SetNonVolatileDirectoryForTest(test_path);
+
+  base::FilePath non_volatile_directory;
+  ASSERT_TRUE(hardware_.GetNonVolatileDirectory(&non_volatile_directory));
+  ASSERT_TRUE(base::CreateDirectory(non_volatile_directory));
+
+  EXPECT_TRUE(base::WriteFile(
+      non_volatile_directory.Append(constants::kRecoveryKeyVersionFileName),
+      "123"));
+
+  std::string version;
+  EXPECT_TRUE(hardware_.GetRecoveryKeyVersion(&version));
+  EXPECT_EQ(std::string("123"), version);
+}
+
+TEST_F(HardwareChromeOSTest, RecoveryKeyVersionTrimWhitespaces) {
+  base::FilePath test_path = root_dir_.GetPath();
+  hardware_.SetNonVolatileDirectoryForTest(test_path);
+
+  base::FilePath non_volatile_directory;
+  ASSERT_TRUE(hardware_.GetNonVolatileDirectory(&non_volatile_directory));
+  ASSERT_TRUE(base::CreateDirectory(non_volatile_directory));
+
+  EXPECT_TRUE(base::WriteFile(
+      non_volatile_directory.Append(constants::kRecoveryKeyVersionFileName),
+      "\n888\n"));
+
+  std::string version;
+  EXPECT_TRUE(hardware_.GetRecoveryKeyVersion(&version));
+  EXPECT_EQ(std::string("888"), version);
+}
+
 }  // namespace chromeos_update_engine
diff --git a/cros/omaha_request_builder_xml.cc b/cros/omaha_request_builder_xml.cc
index c61491a..612761d 100644
--- a/cros/omaha_request_builder_xml.cc
+++ b/cros/omaha_request_builder_xml.cc
@@ -434,20 +434,28 @@
 string OmahaRequestBuilderXml::GetRequest() const {
   auto* system_state = SystemState::Get();
   const auto* params = system_state->request_params();
+
   string os_xml = GetOs();
   string app_xml = GetApps();
   string hw_xml = GetHw();
+  // Valid recovery keys that will be sent are "" or "[0-9]+".
+  string recovery_key_version;
+  if (!system_state->hardware()->GetRecoveryKeyVersion(&recovery_key_version)) {
+    LOG(ERROR) << "Failed to get recovery key version.";
+  }
 
   string request_xml = base::StringPrintf(
       "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"
       "<request requestid=\"%s\" sessionid=\"%s\""
       " protocol=\"3.0\" updater=\"%s\" updaterversion=\"%s\""
-      " installsource=\"%s\" ismachine=\"1\" %s>\n%s%s%s</request>\n",
+      " installsource=\"%s\" ismachine=\"1\" recoverykeyversion=\"%s\" "
+      "%s>\n%s%s%s</request>\n",
       base::GenerateGUID().c_str() /* requestid */,
       session_id_.c_str(),
       constants::kOmahaUpdaterID,
       kOmahaUpdaterVersion,
       params->interactive() ? "ondemandupdate" : "scheduler",
+      recovery_key_version.c_str(),
       (system_state->hardware()->IsRunningFromMiniOs() ? "isminios=\"1\"" : ""),
       os_xml.c_str(),
       app_xml.c_str(),
diff --git a/cros/omaha_request_builder_xml_unittest.cc b/cros/omaha_request_builder_xml_unittest.cc
index e96388b..1892748 100644
--- a/cros/omaha_request_builder_xml_unittest.cc
+++ b/cros/omaha_request_builder_xml_unittest.cc
@@ -187,6 +187,23 @@
   EXPECT_EQ(gen_session_id, session_id);
 }
 
+TEST_F(OmahaRequestBuilderXmlTest, GetRecoveryKeyVersionMissing) {
+  FakeSystemState::Get()->fake_hardware()->SetRecoveryKeyVersion("");
+  OmahaRequestBuilderXml omaha_request{nullptr, false, false, 0, 0, 0, ""};
+  const string request_xml = omaha_request.GetRequest();
+  EXPECT_EQ(1, CountSubstringInString(request_xml, "recoverykeyversion=\"\""))
+      << request_xml;
+}
+
+TEST_F(OmahaRequestBuilderXmlTest, GetRecoveryKeyVersion) {
+  FakeSystemState::Get()->fake_hardware()->SetRecoveryKeyVersion("123");
+  OmahaRequestBuilderXml omaha_request{nullptr, false, false, 0, 0, 0, ""};
+  const string request_xml = omaha_request.GetRequest();
+  const string recovery_key_version =
+      FindAttributeKeyValueInXml(request_xml, "recoverykeyversion", 3);
+  EXPECT_EQ("123", recovery_key_version) << request_xml;
+}
+
 TEST_F(OmahaRequestBuilderXmlTest, GetRequestXmlPlatformUpdateTest) {
   OmahaRequestBuilderXml omaha_request{nullptr, false, false, 0, 0, 0, ""};
   const string request_xml = omaha_request.GetRequest();
diff --git a/cros/platform_constants_chromeos.cc b/cros/platform_constants_chromeos.cc
index cf3f0c3..e760293 100644
--- a/cros/platform_constants_chromeos.cc
+++ b/cros/platform_constants_chromeos.cc
@@ -31,6 +31,7 @@
 const char kCACertificatesPath[] = "/usr/share/chromeos-ca-certificates";
 // This directory is wiped during powerwash.
 const char kNonVolatileDirectory[] = "/var/lib/update_engine";
+const char kRecoveryKeyVersionFileName[] = "recovery_key_version";
 const char kPostinstallMountOptions[] = "";
 
 }  // namespace constants
diff --git a/init/update-engine.conf b/init/update-engine.conf
index 6fd51ee..8a03fb0 100644
--- a/init/update-engine.conf
+++ b/init/update-engine.conf
@@ -38,6 +38,9 @@
 # impact system responsiveness.
 exec ionice -c3 update_engine
 
+env REC_KEY_READABLE=/var/lib/update_engine/recovery_key_readable
+env REC_KEY_VERSION=/var/lib/update_engine/recovery_key_version
+
 # Put update_engine process in its own cgroup.
 # Default cpu.shares is 1024.
 post-start script
@@ -54,4 +57,19 @@
   mkdir -p "${cgroup_net_cls_dir}"
   echo ${pid} > "${cgroup_net_cls_dir}/tasks"
   echo "0x10001" > "${cgroup_net_cls_dir}/net_cls.classid"
+
+  # Run this everytime on boot instead of caching and preserving because
+  # it's possible that the values will change if device is reflashed.
+  recovery_key_tmp="$(mktemp)"
+  flashrom -i GBB:"${recovery_key_tmp}" -r
+  if [ "$?" -ne 0 ]; then
+    logger -t "${UPSTART_JOB}" "Failed to read flashrom."
+    unlink "${recovery_key_tmp}"
+    exit 0
+  fi
+  futility show "${recovery_key_tmp}" > "${REC_KEY_READABLE}"
+  grep -A3 'Recovery Key:' "${REC_KEY_READABLE}" \
+    | grep 'Key Version:' \
+    | grep -Eo '[0-9]+' > "${REC_KEY_VERSION}"
+  unlink "${recovery_key_tmp}"
 end script