arc: arc-data-snapshotd: TakeSnapshot impl
Add implementation of TakeSnapshot method with unit tests.
Extend file_utils by signature and verification operations of snapshot
directory.
BUG=b:160387490
TEST=unit tests
Cq-Depend: chromium:2424223
Change-Id: Ibc3a99063768a1234b51f36e1d1c5e2ca04a71c3
(cherry picked from commit e5307d0b8d0ca023630e596b0f22830c39059235)
Reviewed-on: https://chromium-review.googlesource.com/c/chromiumos/platform2/+/2403541
Tested-by: Polina Bondarenko <pbond@chromium.org>
Auto-Submit: Polina Bondarenko <pbond@chromium.org>
Commit-Queue: Polina Bondarenko <pbond@chromium.org>
Reviewed-by: Jorge Lucangeli Obes <jorgelo@chromium.org>
Reviewed-by: Oleksandr Peletskyi <peletskyi@google.com>
diff --git a/arc/data-snapshotd/BUILD.gn b/arc/data-snapshotd/BUILD.gn
index 0fbe3cf..31f3a22 100644
--- a/arc/data-snapshotd/BUILD.gn
+++ b/arc/data-snapshotd/BUILD.gn
@@ -19,6 +19,7 @@
"libbootlockbox-client",
"libbrillo",
"libchrome-${libbase_ver}",
+ "libselinux",
"openssl",
"protobuf",
"system_api",
@@ -38,7 +39,10 @@
source_set("libarc-data-snapshotd") {
configs += [ ":common_pkg_deps" ]
- sources = [ "dbus_adaptor.cc" ]
+ sources = [
+ "dbus_adaptor.cc",
+ "file_utils.cc",
+ ]
deps = [ ":arc-data-snapshotd-protos" ]
}
@@ -67,8 +71,6 @@
"libbrillo",
"libchrome-${libbase_ver}",
"libchrome-test-${libbase_ver}",
- "libselinux",
- "openssl",
]
}
executable("arc-data-snapshotd_test") {
@@ -77,14 +79,10 @@
"//common-mk:test",
]
deps = [
- ":arc-data-snapshotd-protos",
":data_snapshotd_adaptors",
":libarc-data-snapshotd",
"//common-mk/testrunner:testrunner",
]
- sources = [
- "dbus_adaptor_test.cc",
- "file_utils.cc",
- ]
+ sources = [ "dbus_adaptor_test.cc" ]
}
}
diff --git a/arc/data-snapshotd/dbus_adaptor.cc b/arc/data-snapshotd/dbus_adaptor.cc
index 3c3581a..c6d16cf 100644
--- a/arc/data-snapshotd/dbus_adaptor.cc
+++ b/arc/data-snapshotd/dbus_adaptor.cc
@@ -4,15 +4,19 @@
#include "arc/data-snapshotd/dbus_adaptor.h"
-#include <string>
#include <utility>
+#include <vector>
+#include <base/bind.h>
+#include <base/callback_helpers.h>
#include <base/files/file_util.h>
#include <base/logging.h>
+#include <base/macros.h>
#include <base/memory/ptr_util.h>
+#include <brillo/data_encoding.h>
+#include <brillo/cryptohome.h>
#include <crypto/scoped_openssl_types.h>
#include <crypto/rsa_private_key.h>
-#include <openssl/sha.h>
#include "arc/data-snapshotd/file_utils.h"
#include "bootlockbox-client/bootlockbox/boot_lockbox_client.h"
@@ -27,15 +31,19 @@
"/mnt/stateful_partition/unencrypted/arc-data-snapshot/";
constexpr char kLastSnapshotPath[] = "last";
constexpr char kPreviousSnapshotPath[] = "previous";
+constexpr char kHomeRootDirectory[] = "/home/root";
} // namespace
// BootLockbox snapshot keys:
const char kLastSnapshotPublicKey[] = "snapshot_public_key_last";
const char kPreviousSnapshotPublicKey[] = "snapshot_public_key_previous";
+// Android data directory name:
+const char kAndroidDataDirectory[] = "android-data";
DBusAdaptor::DBusAdaptor()
: DBusAdaptor(base::FilePath(kCommonSnapshotPath),
+ base::FilePath(kHomeRootDirectory),
cryptohome::BootLockboxClient::CreateBootLockboxClient()) {}
DBusAdaptor::~DBusAdaptor() = default;
@@ -43,9 +51,10 @@
// static
std::unique_ptr<DBusAdaptor> DBusAdaptor::CreateForTesting(
const base::FilePath& snapshot_directory,
+ const base::FilePath& home_root_directory,
std::unique_ptr<cryptohome::BootLockboxClient> boot_lockbox_client) {
- return base::WrapUnique(
- new DBusAdaptor(snapshot_directory, std::move(boot_lockbox_client)));
+ return base::WrapUnique(new DBusAdaptor(
+ snapshot_directory, home_root_directory, std::move(boot_lockbox_client)));
}
void DBusAdaptor::RegisterAsync(
@@ -59,7 +68,7 @@
true /* failure_is_fatal */));
}
-bool DBusAdaptor::GenerateKeyPair(brillo::ErrorPtr* error) {
+bool DBusAdaptor::GenerateKeyPair() {
// TODO(b/160387490): Implement showing a spinner screen.
std::string last_public_key_digest;
// Try to move last snapshot to previous for consistency.
@@ -69,7 +78,7 @@
!last_public_key_digest.empty()) {
if (boot_lockbox_client_->Store(kPreviousSnapshotPublicKey,
last_public_key_digest) &&
- ClearSnapshot(error, false /* last */) &&
+ ClearSnapshot(false /* last */) &&
base::Move(last_snapshot_directory_, previous_snapshot_directory_)) {
boot_lockbox_client_->Store(kLastSnapshotPublicKey, "");
} else {
@@ -77,7 +86,7 @@
}
}
// Clear last snapshot - a new one will be created soon.
- ClearSnapshot(error, true /* last */);
+ ClearSnapshot(true /* last */);
// Generate a key pair.
public_key_info_.clear();
@@ -93,16 +102,8 @@
}
// Store a new public key digest.
- std::vector<uint8_t> digest;
- digest.resize(SHA256_DIGEST_LENGTH);
- if (!SHA256((const unsigned char*)public_key_info_.data(),
- public_key_info_.size(), digest.data()) ||
- digest.empty()) {
- LOG(ERROR) << "Failed to calculate digest of public key.";
- return false;
- }
- if (!boot_lockbox_client_->Store(kLastSnapshotPublicKey,
- std::string(digest.begin(), digest.end()))) {
+ std::string encoded_digest = CalculateEncodedSha256Digest(public_key_info_);
+ if (!boot_lockbox_client_->Store(kLastSnapshotPublicKey, encoded_digest)) {
LOG(ERROR) << "Failed to store a public key in BootLockbox.";
return false;
}
@@ -115,7 +116,59 @@
return true;
}
-bool DBusAdaptor::ClearSnapshot(brillo::ErrorPtr* error, bool last) {
+bool DBusAdaptor::TakeSnapshot(const std::string& account_id) {
+ if (!private_key_ || public_key_info_.empty()) {
+ LOG(ERROR) << "Private or public key does not exist.";
+ return false;
+ }
+ if (base::DirectoryExists(last_snapshot_directory_)) {
+ LOG(ERROR) << "Snapshot directory already exists. Should be cleared first.";
+ return false;
+ }
+
+ std::string userhash = brillo::cryptohome::home::SanitizeUserName(account_id);
+ base::FilePath android_data_dir =
+ home_root_directory_.Append(userhash).Append(kAndroidDataDirectory);
+ if (!base::DirectoryExists(android_data_dir)) {
+ LOG(ERROR) << "Android data directory does not exist for user "
+ << account_id;
+ return false;
+ }
+ if (base::IsLink(android_data_dir)) {
+ LOG(ERROR) << android_data_dir.value()
+ << " is a symbolic link, not snapshotting.";
+ return false;
+ }
+
+ if (!base::CopyDirectory(android_data_dir, last_snapshot_directory_,
+ true /* recursive */)) {
+ LOG(ERROR) << "Failed to copy snapshot directory from "
+ << android_data_dir.value() << " to "
+ << last_snapshot_directory_.value();
+ return false;
+ }
+ // This callback will be executed or released before the end of this function.
+ base::ScopedClosureRunner snapshot_clearer(
+ base::BindOnce(base::IgnoreResult(&DBusAdaptor::ClearSnapshot),
+ base::Unretained(this), true /* last */));
+ if (!StorePublicKey(last_snapshot_directory_, public_key_info_))
+ return false;
+ if (!StoreUserhash(last_snapshot_directory_, userhash))
+ return false;
+ if (!SignAndStoreHash(last_snapshot_directory_, private_key_.get(),
+ inode_verification_enabled_)) {
+ return false;
+ }
+ // Snapshot saved correctly, release closure without running it.
+ ignore_result(snapshot_clearer.Release());
+
+ // Dispose keys.
+ private_key_.reset();
+ public_key_info_.clear();
+ return true;
+}
+
+bool DBusAdaptor::ClearSnapshot(bool last) {
base::FilePath dir(last ? last_snapshot_directory_
: previous_snapshot_directory_);
if (!base::DirectoryExists(dir)) {
@@ -131,11 +184,13 @@
DBusAdaptor::DBusAdaptor(
const base::FilePath& snapshot_directory,
+ const base::FilePath& home_root_directory,
std::unique_ptr<cryptohome::BootLockboxClient> boot_lockbox_client)
: org::chromium::ArcDataSnapshotdAdaptor(this),
last_snapshot_directory_(snapshot_directory.Append(kLastSnapshotPath)),
previous_snapshot_directory_(
snapshot_directory.Append(kPreviousSnapshotPath)),
+ home_root_directory_(home_root_directory),
boot_lockbox_client_(std::move(boot_lockbox_client)) {
DCHECK(boot_lockbox_client_);
}
diff --git a/arc/data-snapshotd/dbus_adaptor.h b/arc/data-snapshotd/dbus_adaptor.h
index 409c053..1a7c016 100644
--- a/arc/data-snapshotd/dbus_adaptor.h
+++ b/arc/data-snapshotd/dbus_adaptor.h
@@ -6,6 +6,7 @@
#define ARC_DATA_SNAPSHOTD_DBUS_ADAPTOR_H_
#include <memory>
+#include <string>
#include <vector>
#include <base/files/file_path.h>
@@ -34,6 +35,8 @@
// BootLockbox snapshot keys:
extern const char kLastSnapshotPublicKey[];
extern const char kPreviousSnapshotPublicKey[];
+// Android data directory name:
+extern const char kAndroidDataDirectory[];
// Implements the "org.chromium.ArcDataSnapshotdInterface" D-Bus interface
// exposed by the arc-data-snapshotd daemon (see constants for the API methods
@@ -48,6 +51,7 @@
static std::unique_ptr<DBusAdaptor> CreateForTesting(
const base::FilePath& snapshot_directory,
+ const base::FilePath& home_root_directory,
std::unique_ptr<cryptohome::BootLockboxClient> boot_lockbox_client);
// Registers the D-Bus object that the arc-data-snapshotd daemon exposes and
@@ -57,8 +61,9 @@
// Implementation of the "org.chromium.ArcDataSnapshotdInterface" D-Bus
// interface:
- bool GenerateKeyPair(brillo::ErrorPtr* error) override;
- bool ClearSnapshot(brillo::ErrorPtr* error, bool last) override;
+ bool GenerateKeyPair() override;
+ bool ClearSnapshot(bool last) override;
+ bool TakeSnapshot(const std::string& account_id) override;
const base::FilePath& get_last_snapshot_directory() const {
return last_snapshot_directory_;
@@ -67,9 +72,23 @@
return previous_snapshot_directory_;
}
+ // Use this method only for testing.
+ // Inode verification of snapshot directory is enabled in production by
+ // default.
+ // In production the integrity of the persisting snapshot directory is
+ // verified, inode values should stay the same.
+ //
+ // Using this method, the inode verification for snapshot directories can be
+ // disabled for testing. It is needed to ensure the integrity of snapshot
+ // directories after copying it (inodes change).
+ void set_inode_verification_enabled_for_testing(bool enabled) {
+ inode_verification_enabled_ = enabled;
+ }
+
private:
DBusAdaptor(
const base::FilePath& snapshot_directory,
+ const base::FilePath& home_root_directory,
std::unique_ptr<cryptohome::BootLockboxClient> boot_lockbox_client);
// Manages the D-Bus interfaces exposed by the arc-data-snapshotd daemon.
@@ -78,6 +97,8 @@
// Snapshot directory paths:
const base::FilePath last_snapshot_directory_;
const base::FilePath previous_snapshot_directory_;
+ // Home root directory.
+ const base::FilePath home_root_directory_;
// Manages the communication with BootLockbox.
std::unique_ptr<cryptohome::BootLockboxClient> boot_lockbox_client_;
@@ -88,6 +109,9 @@
// GenerateKeyPair. The key is valid only when |private_key_| is set.
// Should be stored on disk once |private_key_| is disposed.
std::vector<uint8_t> public_key_info_;
+ // Inode verification of snapshot directories is enabled in production ny
+ // default.
+ bool inode_verification_enabled_ = true;
};
} // namespace data_snapshotd
diff --git a/arc/data-snapshotd/dbus_adaptor_test.cc b/arc/data-snapshotd/dbus_adaptor_test.cc
index a379661..1730394 100644
--- a/arc/data-snapshotd/dbus_adaptor_test.cc
+++ b/arc/data-snapshotd/dbus_adaptor_test.cc
@@ -9,6 +9,8 @@
#include <base/files/file_path.h>
#include <base/files/file_util.h>
#include <base/files/scoped_temp_dir.h>
+#include <brillo/cryptohome.h>
+#include <brillo/data_encoding.h>
#include <dbus/bus.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
@@ -34,10 +36,11 @@
namespace {
constexpr char kRandomDir[] = "data";
-constexpr char kRandomFile[] = "hash";
+constexpr char kRandomFile[] = "random file";
constexpr char kContent[] = "content";
constexpr char kFakeLastSnapshotPublicKey[] = "fake_public_key";
-
+constexpr char kFakeAccountID[] = "fake_account_id";
+constexpr char kFakeAccountID2[] = "fake_aacount_id_2";
MATCHER_P(nEq, expected, "") {
return expected != arg;
}
@@ -63,20 +66,36 @@
class DBusAdaptorTest : public testing::Test {
public:
DBusAdaptorTest() : bus_(new dbus::Bus{dbus::Bus::Options{}}) {
+ brillo::cryptohome::home::SetSystemSalt(&salt_);
EXPECT_TRUE(root_tempdir_.CreateUniqueTempDir());
+
auto boot_lockbox_client =
std::make_unique<testing::StrictMock<MockBootLockboxClient>>(bus_);
boot_lockbox_client_ = boot_lockbox_client.get();
dbus_adaptor_ = DBusAdaptor::CreateForTesting(
- root_tempdir_.GetPath(), std::move(boot_lockbox_client));
+ root_tempdir_.GetPath(), root_tempdir_.GetPath(),
+ std::move(boot_lockbox_client));
+
+ user_directory_ = root_tempdir_.GetPath().Append(hash(kFakeAccountID));
+ EXPECT_TRUE(base::CreateDirectory(user_directory_));
}
DBusAdaptor* dbus_adaptor() { return dbus_adaptor_.get(); }
- const base::FilePath& last_snapshot_dir() {
+ const base::FilePath& last_snapshot_dir() const {
return dbus_adaptor_->get_last_snapshot_directory();
}
- const base::FilePath& previous_snapshot_dir() {
+ const base::FilePath& previous_snapshot_dir() const {
return dbus_adaptor_->get_previous_snapshot_directory();
}
+ base::FilePath android_data_dir() const {
+ return user_directory_.Append(kAndroidDataDirectory);
+ }
+ base::FilePath random_dir() const {
+ return root_tempdir_.GetPath().Append(kRandomDir);
+ }
+ std::string hash(const std::string& account_id) const {
+ return brillo::cryptohome::home::SanitizeUserName(account_id);
+ }
+
// Creates |dir| and fills in with random content.
void CreateDir(const base::FilePath& dir) {
EXPECT_TRUE(base::CreateDirectory(dir));
@@ -88,10 +107,12 @@
MockBootLockboxClient* boot_lockbox_client() { return boot_lockbox_client_; }
private:
+ std::string salt_ = "salt";
scoped_refptr<dbus::Bus> bus_;
MockBootLockboxClient* boot_lockbox_client_;
std::unique_ptr<DBusAdaptor> dbus_adaptor_;
base::ScopedTempDir root_tempdir_;
+ base::FilePath user_directory_;
};
TEST_F(DBusAdaptorTest, ClearSnapshotBasic) {
@@ -101,15 +122,15 @@
CreateDir(previous_snapshot_dir());
EXPECT_TRUE(base::DirectoryExists(previous_snapshot_dir()));
- EXPECT_TRUE(dbus_adaptor()->ClearSnapshot(nullptr, false /* last */));
+ EXPECT_TRUE(dbus_adaptor()->ClearSnapshot(false /* last */));
EXPECT_FALSE(base::DirectoryExists(previous_snapshot_dir()));
- EXPECT_TRUE(dbus_adaptor()->ClearSnapshot(nullptr, false /* last */));
+ EXPECT_TRUE(dbus_adaptor()->ClearSnapshot(false /* last */));
- EXPECT_TRUE(dbus_adaptor()->ClearSnapshot(nullptr, true /* last */));
+ EXPECT_TRUE(dbus_adaptor()->ClearSnapshot(true /* last */));
EXPECT_FALSE(base::DirectoryExists(last_snapshot_dir()));
- EXPECT_TRUE(dbus_adaptor()->ClearSnapshot(nullptr, true /* last */));
+ EXPECT_TRUE(dbus_adaptor()->ClearSnapshot(true /* last */));
}
// Test successful basic flow with no pre-existing snapshots.
@@ -117,7 +138,7 @@
EXPECT_CALL(*boot_lockbox_client(), Store(Eq(kLastSnapshotPublicKey), _))
.WillOnce(Return(true));
EXPECT_CALL(*boot_lockbox_client(), Finalize()).WillOnce(Return(true));
- EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair(nullptr));
+ EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair());
}
// Test successful basic flow with pre-existing snapshots.
@@ -138,7 +159,6 @@
CalculateDirectoryCryptographicHash(previous_dir);
EXPECT_FALSE(previous_hash.empty());
- EXPECT_NE(previous_dir.DebugString(), last_dir.DebugString());
EXPECT_NE(last_hash, previous_hash);
}
@@ -158,7 +178,7 @@
.WillOnce(Return(true));
EXPECT_CALL(*boot_lockbox_client(), Finalize()).WillOnce(Return(true));
- EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair(nullptr));
+ EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair());
{
SnapshotDirectory previous_dir;
EXPECT_TRUE(ReadSnapshotDirectory(previous_snapshot_dir(), &previous_dir));
@@ -167,7 +187,6 @@
EXPECT_FALSE(previous_hash.empty());
// Check that the last snapshot has been moved to previous snapshot dir.
EXPECT_EQ(previous_hash, last_hash);
- EXPECT_EQ(previous_dir.DebugString(), last_dir.DebugString());
}
}
@@ -177,15 +196,14 @@
// Attempt failure: last snapshot dir => previous snapshot dir.
EXPECT_CALL(*boot_lockbox_client(), Read(Eq(kLastSnapshotPublicKey), _))
- .WillOnce(Invoke(
- [](const std::string& key, std::string* value) { return false; }));
+ .WillOnce(Return(false));
EXPECT_CALL(*boot_lockbox_client(),
Store(Eq(kLastSnapshotPublicKey), nEq("")))
.WillOnce(Return(true));
EXPECT_CALL(*boot_lockbox_client(), Finalize()).WillOnce(Return(true));
// Generating key pair should be still successful.
- EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair(nullptr));
+ EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair());
}
// Test successful flow with pre-existing last snapshot empty key.
@@ -204,7 +222,7 @@
EXPECT_CALL(*boot_lockbox_client(), Finalize()).WillOnce(Return(true));
// Generating key pair should be still successful.
- EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair(nullptr));
+ EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair());
}
// Test success flow with pre-existing snapshots and moving error.
@@ -228,7 +246,7 @@
// Generating key pair should be still successful, because the last snapshot
// will be re-generated anyway.
- EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair(nullptr));
+ EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair());
}
// Test failure flow when storing freshly generated public key is failed.
@@ -238,7 +256,7 @@
Store(Eq(kLastSnapshotPublicKey), nEq("")))
.WillOnce(Return(false));
- EXPECT_FALSE(dbus_adaptor()->GenerateKeyPair(nullptr));
+ EXPECT_FALSE(dbus_adaptor()->GenerateKeyPair());
}
// Test failure flow when finalizing BootLockbox is attempted.
@@ -249,7 +267,143 @@
// Fail once the finalization of BootLockbox is attempted.
EXPECT_CALL(*boot_lockbox_client(), Finalize()).WillOnce(Return(false));
- EXPECT_FALSE(dbus_adaptor()->GenerateKeyPair(nullptr));
+ EXPECT_FALSE(dbus_adaptor()->GenerateKeyPair());
+}
+
+// Test failure flow when the keys were not generated.
+TEST_F(DBusAdaptorTest, TakeSnapshotNoPrivateKeyFailure) {
+ EXPECT_FALSE(dbus_adaptor()->TakeSnapshot(kFakeAccountID));
+}
+
+// Test failure flow when the last snapshot directory already exists.
+TEST_F(DBusAdaptorTest, TakeSnapshotLastSnapshotExistFailure) {
+ EXPECT_CALL(*boot_lockbox_client(), Store(Eq(kLastSnapshotPublicKey), _))
+ .WillOnce(Return(true));
+ EXPECT_CALL(*boot_lockbox_client(), Finalize()).WillOnce(Return(true));
+ EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair());
+
+ CreateDir(last_snapshot_dir());
+ EXPECT_TRUE(base::DirectoryExists(last_snapshot_dir()));
+ EXPECT_FALSE(dbus_adaptor()->TakeSnapshot(kFakeAccountID));
+}
+
+// Test failure flow when android-data directory does not exist.
+TEST_F(DBusAdaptorTest, TakeSnapshotAndroidDataDirNotExist) {
+ EXPECT_CALL(*boot_lockbox_client(), Store(Eq(kLastSnapshotPublicKey), _))
+ .WillOnce(Return(true));
+ EXPECT_CALL(*boot_lockbox_client(), Finalize()).WillOnce(Return(true));
+ EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair());
+ EXPECT_FALSE(base::DirectoryExists(android_data_dir()));
+
+ EXPECT_FALSE(dbus_adaptor()->TakeSnapshot(kFakeAccountID));
+}
+
+// Test failure flow when android-data is file.
+TEST_F(DBusAdaptorTest, TakeSnapshotAndroidDataNotDirFile) {
+ EXPECT_CALL(*boot_lockbox_client(), Store(Eq(kLastSnapshotPublicKey), _))
+ .WillOnce(Return(true));
+ EXPECT_CALL(*boot_lockbox_client(), Finalize()).WillOnce(Return(true));
+ EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair());
+ // Create a file instead of android-data directory.
+ EXPECT_TRUE(base::WriteFile(android_data_dir(), kContent, strlen(kContent)));
+ EXPECT_TRUE(base::PathExists(android_data_dir()));
+ EXPECT_FALSE(base::DirectoryExists(android_data_dir()));
+
+ EXPECT_FALSE(dbus_adaptor()->TakeSnapshot(kFakeAccountID));
+}
+
+// Test failure flow when android-data is a sym link.
+TEST_F(DBusAdaptorTest, TakeSnapshotAndroidDataSymLink) {
+ EXPECT_CALL(*boot_lockbox_client(), Store(Eq(kLastSnapshotPublicKey), _))
+ .WillOnce(Return(true));
+ EXPECT_CALL(*boot_lockbox_client(), Finalize()).WillOnce(Return(true));
+ EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair());
+ // Create a symlink.
+ CreateDir(random_dir());
+ EXPECT_TRUE(base::CreateSymbolicLink(random_dir(), android_data_dir()));
+ EXPECT_TRUE(base::IsLink(android_data_dir()));
+ EXPECT_TRUE(base::DirectoryExists(android_data_dir()));
+
+ EXPECT_FALSE(dbus_adaptor()->TakeSnapshot(kFakeAccountID));
+}
+
+// Test failure flow when android-data is a fifo.
+TEST_F(DBusAdaptorTest, TakeSnapshotAndroidDataFiFo) {
+ EXPECT_CALL(*boot_lockbox_client(), Store(Eq(kLastSnapshotPublicKey), _))
+ .WillOnce(Return(true));
+ EXPECT_CALL(*boot_lockbox_client(), Finalize()).WillOnce(Return(true));
+ EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair());
+ // Create a fifo android-data.
+ mkfifo(android_data_dir().value().c_str(), 0666);
+ EXPECT_TRUE(base::PathExists(android_data_dir()));
+ EXPECT_FALSE(base::DirectoryExists(android_data_dir()));
+
+ EXPECT_FALSE(dbus_adaptor()->TakeSnapshot(kFakeAccountID));
+}
+
+// Test basic TakeSnapshot success flow.
+TEST_F(DBusAdaptorTest, TakeSnapshotSuccess) {
+ // In this test the copied snapshot directory is verified against the origin
+ // android data directory. Inodes verification must be disabled, because the
+ // inode values are changed after copying.
+ // In production, it is not the case, because the directorys' integrity is
+ // verified against itself and inode values should persist.
+ dbus_adaptor()->set_inode_verification_enabled_for_testing(
+ false /* enabled */);
+ std::string expected_public_key_digest;
+ EXPECT_CALL(*boot_lockbox_client(), Store(Eq(kLastSnapshotPublicKey), _))
+ .WillOnce(Invoke([&expected_public_key_digest](
+ const std::string& key, const std::string& digest) {
+ expected_public_key_digest = digest;
+ return true;
+ }));
+ EXPECT_CALL(*boot_lockbox_client(), Finalize()).WillOnce(Return(true));
+ EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair());
+
+ CreateDir(android_data_dir());
+ EXPECT_TRUE(base::DirectoryExists(android_data_dir()));
+ // Store userhash to ensure that userhash stays the same.
+ EXPECT_TRUE(StoreUserhash(android_data_dir(), hash(kFakeAccountID)));
+ SnapshotDirectory android_dir;
+ EXPECT_TRUE(ReadSnapshotDirectory(android_data_dir(), &android_dir,
+ false /* inode_verification_enabled */));
+ std::vector<uint8_t> android_data_hash =
+ CalculateDirectoryCryptographicHash(android_dir);
+ EXPECT_FALSE(android_data_hash.empty());
+
+ EXPECT_TRUE(dbus_adaptor()->TakeSnapshot(kFakeAccountID));
+ EXPECT_TRUE(base::DirectoryExists(last_snapshot_dir()));
+ SnapshotDirectory last_dir;
+ EXPECT_TRUE(ReadSnapshotDirectory(last_snapshot_dir(), &last_dir,
+ false /* inode_verification_enabled */));
+ std::vector<uint8_t> last_snapshot_hash =
+ CalculateDirectoryCryptographicHash(last_dir);
+ EXPECT_FALSE(last_snapshot_hash.empty());
+ EXPECT_EQ(android_data_hash, last_snapshot_hash);
+
+ // Verification for another account ID should fail.
+ EXPECT_FALSE(VerifyHash(last_snapshot_dir(), hash(kFakeAccountID2),
+ expected_public_key_digest,
+ false /* inode_verification_enabled */));
+ EXPECT_TRUE(VerifyHash(last_snapshot_dir(), hash(kFakeAccountID),
+ expected_public_key_digest,
+ false /* inode_verification_enabled */));
+ dbus_adaptor()->set_inode_verification_enabled_for_testing(
+ true /* enabled */);
+}
+
+// Test failure flow if TakeSnapshot is invoked twice.
+TEST_F(DBusAdaptorTest, TakeSnapshotDouble) {
+ EXPECT_CALL(*boot_lockbox_client(), Store(Eq(kLastSnapshotPublicKey), _))
+ .WillOnce(Return(true));
+ EXPECT_CALL(*boot_lockbox_client(), Finalize()).WillOnce(Return(true));
+ EXPECT_TRUE(dbus_adaptor()->GenerateKeyPair());
+
+ CreateDir(android_data_dir());
+ EXPECT_TRUE(dbus_adaptor()->TakeSnapshot(kFakeAccountID));
+
+ CreateDir(android_data_dir());
+ EXPECT_FALSE(dbus_adaptor()->TakeSnapshot(kFakeAccountID));
}
} // namespace data_snapshotd
diff --git a/arc/data-snapshotd/dbus_bindings/org.chromium.ArcDataSnapshotd.xml b/arc/data-snapshotd/dbus_bindings/org.chromium.ArcDataSnapshotd.xml
index 3fb74ac..03a23b9 100644
--- a/arc/data-snapshotd/dbus_bindings/org.chromium.ArcDataSnapshotd.xml
+++ b/arc/data-snapshotd/dbus_bindings/org.chromium.ArcDataSnapshotd.xml
@@ -12,17 +12,47 @@
Public key is stored in BootlockBox.
The method should be called before the start of user session.
</tp:docstring>
+ <annotation name="org.chromium.DBus.Method.Kind" value="simple"/>
+ <arg type="b" name="success" direction="out">
+ <tp:docstring>
+ Result of the operation.
+ </tp:docstring>
+ </arg>
</method>
<method name="ClearSnapshot">
<tp:docstring>
Clears snapshot.
</tp:docstring>
+ <annotation name="org.chromium.DBus.Method.Kind" value="simple"/>
<arg type="b" name="last" direction="in">
<tp:docstring>
Specifies a snapshot to clear.
If true, the last snapshot directory is cleared.
</tp:docstring>
</arg>
+ <arg type="b" name="success" direction="out">
+ <tp:docstring>
+ Result of the operation.
+ </tp:docstring>
+ </arg>
+ </method>
+ <method name="TakeSnapshot">
+ <tp:docstring>
+ Saves and signs a snapshot.
+ </tp:docstring>
+ <annotation name="org.chromium.DBus.Method.Kind" value="simple"/>
+ <arg type="s" name="account_id" direction="in">
+ <tp:docstring>
+ Specifies an account ID of a user, that a snapshot is taken for.
+ The caller (Chrome) is responsible for executing this method only for
+ managed guest session account.
+ </tp:docstring>
+ </arg>
+ <arg type="b" name="success" direction="out">
+ <tp:docstring>
+ Result of the operation.
+ </tp:docstring>
+ </arg>
</method>
</interface>
</node>
diff --git a/arc/data-snapshotd/file_utils.cc b/arc/data-snapshotd/file_utils.cc
index f34387b..9b6a231 100644
--- a/arc/data-snapshotd/file_utils.cc
+++ b/arc/data-snapshotd/file_utils.cc
@@ -5,7 +5,6 @@
#include "arc/data-snapshotd/file_utils.h"
#include <algorithm>
-#include <string>
#include <utility>
#if USE_SELINUX
@@ -19,13 +18,26 @@
#include <base/files/file_enumerator.h>
#include <base/files/file_util.h>
#include <base/logging.h>
+#include <brillo/data_encoding.h>
+#include <crypto/rsa_private_key.h>
+#include <crypto/signature_creator.h>
+#include <crypto/signature_verifier.h>
#include <openssl/sha.h>
namespace arc {
namespace data_snapshotd {
+namespace {
+
+constexpr char kHashFile[] = "hash";
+constexpr char kPublicKeyFile[] = "public_key_info";
+constexpr char kUserhashFile[] = "userhash";
+
+} // namespace
+
bool ReadSnapshotDirectory(const base::FilePath& dir,
- SnapshotDirectory* snapshot_directory) {
+ SnapshotDirectory* snapshot_directory,
+ bool inode_verification_enabled) {
if (!snapshot_directory) {
LOG(ERROR) << "snapshot_directory is nullptr";
return false;
@@ -38,10 +50,13 @@
std::vector<SnapshotFile> snapshot_files;
for (auto file = dir_enumerator.Next(); !file.empty();
file = dir_enumerator.Next()) {
- std::string relative_path =
- file.value().substr(dir.value().size(), std::string::npos);
+ base::FilePath relative_path;
+ if (!dir.IsParent(file) || !dir.AppendRelativePath(file, &relative_path)) {
+ LOG(ERROR) << dir.value() << " is not a parent of " << file.value();
+ return false;
+ }
SnapshotFile snapshot_file;
- snapshot_file.set_name(std::move(relative_path));
+ snapshot_file.set_name(relative_path.value());
std::string contents;
if (!dir_enumerator.GetInfo().IsDirectory() &&
!base::ReadFileToString(file, &contents)) {
@@ -69,7 +84,7 @@
if (con != nullptr) {
free(con);
}
-#endif
+#endif // USE_SELINUX
struct stat stat_buf;
if (lstat(file.value().c_str(), &stat_buf)) {
@@ -77,7 +92,8 @@
return false;
}
Stat* stat_value = snapshot_file.mutable_stat();
- stat_value->set_ino(stat_buf.st_ino);
+ if (inode_verification_enabled)
+ stat_value->set_ino(stat_buf.st_ino);
stat_value->set_mode(stat_buf.st_mode);
stat_value->set_uid(stat_buf.st_uid);
stat_value->set_gid(stat_buf.st_gid);
@@ -87,9 +103,14 @@
}
std::sort(snapshot_files.begin(), snapshot_files.end(),
[](const SnapshotFile& a, const SnapshotFile& b) {
+ // Sort lexicographically by name.
return a.name() < b.name();
});
for (auto file : snapshot_files) {
+ if (file.name() == kHashFile)
+ continue;
+ if (file.name() == kPublicKeyFile)
+ continue;
*snapshot_directory->mutable_files()->Add() = file;
}
return true;
@@ -105,12 +126,180 @@
}
hash.resize(SHA256_DIGEST_LENGTH);
if (!SHA256((const unsigned char*)serialized.data(), serialized.size(),
- hash.data())) {
+ hash.data()) ||
+ hash.empty()) {
LOG(ERROR) << "Failed to calculate digest of serialized SnapshotDirectory.";
return std::vector<uint8_t>();
}
return hash;
}
+bool StorePublicKey(const base::FilePath& dir,
+ const std::vector<uint8_t>& public_key_info) {
+ if (public_key_info.empty()) {
+ LOG(ERROR) << "Empty public key info";
+ return false;
+ }
+ if (!base::DirectoryExists(dir)) {
+ LOG(ERROR) << "Directory " << dir.value() << " does not exist.";
+ return false;
+ }
+ std::string encoded_public_key = brillo::data_encoding::Base64Encode(
+ public_key_info.data(), public_key_info.size());
+ if (!base::WriteFile(dir.Append(kPublicKeyFile), encoded_public_key.data(),
+ encoded_public_key.length())) {
+ LOG(ERROR) << "Failed to write public key info to file "
+ << dir.Append(kPublicKeyFile);
+ return false;
+ }
+ return true;
+}
+
+bool StoreUserhash(const base::FilePath& dir, const std::string& userhash) {
+ if (userhash.empty()) {
+ LOG(ERROR) << "Empty user hash";
+ return false;
+ }
+ if (!base::DirectoryExists(dir)) {
+ LOG(ERROR) << "Directory " << dir.value() << " does not exist.";
+ return false;
+ }
+ if (!base::WriteFile(dir.Append(kUserhashFile), userhash.c_str(),
+ userhash.size())) {
+ LOG(ERROR) << "Failed to write userhash to file "
+ << dir.Append(kUserhashFile);
+ return false;
+ }
+ return true;
+}
+
+bool SignAndStoreHash(const base::FilePath& dir,
+ crypto::RSAPrivateKey* private_key,
+ bool inode_verification_enabled) {
+ if (!private_key) {
+ LOG(ERROR) << "nullptr private key";
+ return false;
+ }
+ if (!base::DirectoryExists(dir)) {
+ LOG(ERROR) << "Directory " << dir.value() << " does not exist.";
+ return false;
+ }
+ SnapshotDirectory snapshot_dir;
+ if (!ReadSnapshotDirectory(dir, &snapshot_dir, inode_verification_enabled)) {
+ return false;
+ }
+ std::vector<uint8_t> hash = CalculateDirectoryCryptographicHash(snapshot_dir);
+ if (hash.empty()) {
+ return false;
+ }
+ std::vector<uint8_t> signature;
+ if (!crypto::SignatureCreator::Sign(
+ private_key, crypto::SignatureCreator::HashAlgorithm::SHA256,
+ hash.data(), hash.size(), &signature)) {
+ LOG(ERROR) << "Failed to sign directory contents: " << dir.value();
+ return false;
+ }
+ std::string encoded_signature =
+ brillo::data_encoding::Base64Encode(signature.data(), signature.size());
+ if (!base::WriteFile(dir.Append(kHashFile), encoded_signature.data(),
+ encoded_signature.length())) {
+ LOG(ERROR) << "Failed to write a signature to file "
+ << dir.Append(kHashFile);
+ return false;
+ }
+ return true;
+}
+
+bool VerifyHash(const base::FilePath& dir,
+ const std::string& expected_userhash,
+ const std::string& expected_public_key_digest,
+ bool inode_verification_enabled) {
+ if (!base::DirectoryExists(dir)) {
+ LOG(ERROR) << "Directory " << dir.value() << " does not exist.";
+ return false;
+ }
+ if (expected_public_key_digest.empty()) {
+ LOG(ERROR) << "Public key digest is empty.";
+ return false;
+ }
+ std::string userhash;
+ if (!base::ReadFileToString(dir.Append(kUserhashFile), &userhash)) {
+ LOG(ERROR) << "Failed to read userhash for file "
+ << dir.Append(kUserhashFile);
+ return false;
+ }
+ if (userhash != expected_userhash) {
+ LOG(ERROR) << "Requested to load snapshot for unsupported account.";
+ return false;
+ }
+ std::string encoded_public_key;
+ if (!base::ReadFileToString(dir.Append(kPublicKeyFile),
+ &encoded_public_key)) {
+ LOG(ERROR) << "Failed to read public key info from file "
+ << dir.Append(kPublicKeyFile);
+ return false;
+ }
+ std::vector<uint8_t> public_key;
+ if (!brillo::data_encoding::Base64Decode(encoded_public_key, &public_key)) {
+ LOG(ERROR) << "Failed to decode public key.";
+ return false;
+ }
+ std::string encoded_public_key_digest =
+ CalculateEncodedSha256Digest(public_key);
+ if (encoded_public_key_digest.empty()) {
+ return false;
+ }
+ if (encoded_public_key_digest.compare(expected_public_key_digest)) {
+ LOG(ERROR) << "Public key has been modified.";
+ return false;
+ }
+
+ std::string contents;
+ if (!base::ReadFileToString(dir.Append(kHashFile), &contents)) {
+ LOG(ERROR) << "Failed to read signed hash from file "
+ << dir.Append(kHashFile);
+ return false;
+ }
+ std::vector<uint8_t> signature;
+ if (!brillo::data_encoding::Base64Decode(contents, &signature)) {
+ LOG(ERROR) << "Failed to decode signature.";
+ return false;
+ }
+ crypto::SignatureVerifier verifier;
+ if (!verifier.VerifyInit(
+ crypto::SignatureVerifier::SignatureAlgorithm::RSA_PKCS1_SHA256,
+ signature.data(), signature.size(), public_key.data(),
+ public_key.size())) {
+ LOG(ERROR) << "Failed to initilize signature verifier.";
+ return false;
+ }
+
+ SnapshotDirectory snapshot_dir;
+ if (!ReadSnapshotDirectory(dir, &snapshot_dir, inode_verification_enabled)) {
+ return false;
+ }
+ std::string serialized;
+ if (!snapshot_dir.SerializeToString(&serialized)) {
+ LOG(ERROR) << "Failed to serialize snapshot dir";
+ return false;
+ }
+ verifier.VerifyUpdate(reinterpret_cast<const uint8_t*>(serialized.c_str()),
+ serialized.size());
+ return verifier.VerifyFinal();
+}
+
+std::string CalculateEncodedSha256Digest(const std::vector<uint8_t>& value) {
+ // Store a new public key digest.
+ std::vector<uint8_t> digest;
+ digest.resize(SHA256_DIGEST_LENGTH);
+ if (!SHA256((const unsigned char*)value.data(), value.size(),
+ digest.data()) ||
+ digest.empty()) {
+ LOG(ERROR) << "Failed to calculate digest of public key.";
+ return "";
+ }
+ return brillo::data_encoding::Base64Encode(digest.data(), digest.size());
+}
+
} // namespace data_snapshotd
} // namespace arc
diff --git a/arc/data-snapshotd/file_utils.h b/arc/data-snapshotd/file_utils.h
index 039257b..7b06644 100644
--- a/arc/data-snapshotd/file_utils.h
+++ b/arc/data-snapshotd/file_utils.h
@@ -5,26 +5,75 @@
#ifndef ARC_DATA_SNAPSHOTD_FILE_UTILS_H_
#define ARC_DATA_SNAPSHOTD_FILE_UTILS_H_
+#include <string>
#include <vector>
#include <base/files/file_path.h>
#include "proto/directory.pb.h"
+namespace crypto {
+
+class RSAPrivateKey;
+
+} // namespace crypto
+
namespace arc {
namespace data_snapshotd {
// Extracts all files and file info for all files from |dir| and fills in
// |snapshot_directory| object, that should be non-nullptr.
+// Set |inode_verification_enabled| to false only for testing to disable the
+// inode integrity check for snapshot directories.
// Returns true in case of success and false in case of any error.
bool ReadSnapshotDirectory(const base::FilePath& dir,
- SnapshotDirectory* snapshot_directory);
+ SnapshotDirectory* snapshot_directory,
+ bool inode_verification_enabled = true);
// Calculates SHA256 hash for serialized |dir|.
// In case of any error returns empty hash.
std::vector<uint8_t> CalculateDirectoryCryptographicHash(
const SnapshotDirectory& dir);
+// Encodes and stores |public_key_info| on disk in |dir|/public_key_info file.
+// |dir| is an existing directory, where a prospective snapshot signed with
+// a corresponding private key will be stored.
+// Returns false in case of any error.
+bool StorePublicKey(const base::FilePath& dir,
+ const std::vector<uint8_t>& public_key_info);
+
+// Stores |userhash| on disk in |dir|/userhash file.
+// |dir| is an existing directory, where a prospective snapshot signed with
+// a corresponding private key will be stored.
+// Returns false in case of any error.
+bool StoreUserhash(const base::FilePath& dir, const std::string& userhash);
+
+// Calculates SHA256 hash for |dir| (excluding |dir|/hash file), signs it with
+// |private_key| and stores in |dir|/hash file.
+// Set |inode_verification_enabled| to false only for testing to disable the
+// inode integrity check for snapshot directories.
+// Returns false in case of any error.
+bool SignAndStoreHash(const base::FilePath& dir,
+ crypto::RSAPrivateKey* private_key,
+ bool inode_verification_enabled);
+
+// Verifies signed hash stored in a |dir|/hash file by public key stored in a
+// |dir|/public_key_info.
+// Verifies |expected_userhash| is equal to the userhash stored in
+// |dir|/userhash.
+// Verifies integrity of |dir|/public_key_info by sha256 hash
+// |expected_public_key_digest|.
+// Set |inode_verification_enabled| to false only for testing to disable the
+// inode integrity check for snapshot directories.
+// Returns false in case of any error.
+bool VerifyHash(const base::FilePath& dir,
+ const std::string& expected_userhash,
+ const std::string& expected_public_key_digest,
+ bool inode_verification_enabled);
+
+// Calculates SHA256 hash of |value| and encodes it.
+// Returns an empty string in case of any error.
+std::string CalculateEncodedSha256Digest(const std::vector<uint8_t>& value);
} // namespace data_snapshotd
} // namespace arc
diff --git a/arc/data-snapshotd/init/arc-data-snapshotd.conf b/arc/data-snapshotd/init/arc-data-snapshotd.conf
index 83892a6..0a8c5a9 100644
--- a/arc/data-snapshotd/init/arc-data-snapshotd.conf
+++ b/arc/data-snapshotd/init/arc-data-snapshotd.conf
@@ -37,6 +37,8 @@
# -b /run/dbus: shared socket file for talking with the D-Bus daemon;
# -b /mnt/stateful_partition/unencrypted/arc-data-snapshot: arc data snapshot
# directory;
+# -b /opt/google/containers/android/rootfs/android-data: bind mounted
+# android-data directory;
# -S: apply seccomp filters.
script
logger -t "${UPSTART_JOB}" "Start arc-data-snapshotd"
@@ -48,6 +50,7 @@
-k 'tmpfs,/run,tmpfs,MS_NODEV|MS_NOSUID|MS_NOEXEC,mode=755,size=10M' \
-b /run/dbus \
-b /mnt/stateful_partition/unencrypted/arc-data-snapshot \
+ -b /opt/google/containers/android/rootfs/android-data \
-S /usr/share/policy/arc-data-snapshotd-seccomp.policy \
-- /usr/bin/arc-data-snapshotd
end script