blob: 041b9b696415aeafe933fcadf30f1d6d7faf878a [file] [log] [blame]
// Copyright 2020 The Chromium OS Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// Unit tests for UserSession.
#include "cryptohome/user_session.h"
#include <string>
#include <vector>
#include <base/memory/ref_counted.h>
#include <base/test/task_environment.h>
#include <brillo/cryptohome.h>
#include <brillo/secure_blob.h>
#include <gtest/gtest.h>
#include <policy/libpolicy.h>
#include <policy/mock_device_policy.h>
#include "cryptohome/credentials.h"
#include "cryptohome/crypto.h"
#include "cryptohome/crypto/hmac.h"
#include "cryptohome/crypto/secure_blob_util.h"
#include "cryptohome/filesystem_layout.h"
#include "cryptohome/keyset_management.h"
#include "cryptohome/mock_keyset_management.h"
#include "cryptohome/mock_platform.h"
#include "cryptohome/pkcs11/fake_pkcs11_token.h"
#include "cryptohome/pkcs11/mock_pkcs11_token_factory.h"
#include "cryptohome/storage/homedirs.h"
#include "cryptohome/storage/mock_mount.h"
using brillo::SecureBlob;
using ::testing::_;
using ::testing::ByRef;
using ::testing::DoAll;
using ::testing::Eq;
using ::testing::Invoke;
using ::testing::NiceMock;
using ::testing::Return;
using ::testing::SetArgPointee;
namespace cryptohome {
namespace {
constexpr char kUser0[] = "First User";
constexpr char kUserPassword0[] = "user0_pass";
constexpr char kWebAuthnSecretHmacMessage[] = "AuthTimeWebAuthnSecret";
} // namespace
class UserSessionTest : public ::testing::Test {
public:
UserSessionTest() : crypto_(&platform_) {}
~UserSessionTest() override {}
// Not copyable or movable
UserSessionTest(const UserSessionTest&) = delete;
UserSessionTest& operator=(const UserSessionTest&) = delete;
UserSessionTest(UserSessionTest&&) = delete;
UserSessionTest& operator=(UserSessionTest&&) = delete;
void SetUp() override {
InitializeFilesystemLayout(&platform_, &crypto_, &system_salt_);
keyset_management_ = std::make_unique<KeysetManagement>(
&platform_, &crypto_, system_salt_,
std::make_unique<VaultKeysetFactory>());
user_activity_timestamp_manager_ =
std::make_unique<UserOldestActivityTimestampManager>(&platform_);
HomeDirs::RemoveCallback remove_callback;
mock_device_policy_ = new policy::MockDevicePolicy();
homedirs_ = std::make_unique<HomeDirs>(
&platform_,
std::make_unique<policy::PolicyProvider>(
std::unique_ptr<policy::MockDevicePolicy>(mock_device_policy_)),
remove_callback);
platform_.GetFake()->SetSystemSaltForLibbrillo(system_salt_);
AddUser(kUser0, kUserPassword0);
PrepareDirectoryStructure();
homedirs_->Create(kUser0);
keyset_management_->AddInitialKeyset(users_[0].credentials);
mount_ = new NiceMock<MockMount>();
ON_CALL(pkcs11_token_factory_, New(_, _, _))
.WillByDefault(Invoke([](const std::string& username,
const base::FilePath& token_dir,
const brillo::SecureBlob& auth_data) {
return std::make_unique<FakePkcs11Token>();
}));
session_ = new UserSession(homedirs_.get(), keyset_management_.get(),
user_activity_timestamp_manager_.get(),
&pkcs11_token_factory_, system_salt_, mount_);
}
void TearDown() override {
platform_.GetFake()->RemoveSystemSaltForLibbrillo();
}
protected:
struct UserInfo {
std::string name;
std::string obfuscated;
brillo::SecureBlob passkey;
Credentials credentials;
base::FilePath homedir_path;
base::FilePath user_path;
};
// Information about users' homedirs. The order of users is equal to kUsers.
std::vector<UserInfo> users_;
NiceMock<MockPlatform> platform_;
NiceMock<MockPkcs11TokenFactory> pkcs11_token_factory_;
Crypto crypto_;
brillo::SecureBlob system_salt_;
std::unique_ptr<KeysetManagement> keyset_management_;
policy::MockDevicePolicy* mock_device_policy_; // owned by homedirs_
std::unique_ptr<UserOldestActivityTimestampManager>
user_activity_timestamp_manager_;
std::unique_ptr<HomeDirs> homedirs_;
scoped_refptr<UserSession> session_;
// TODO(dlunev): Replace with real mount when FakePlatform is mature enough
// to support it mock-less.
scoped_refptr<MockMount> mount_;
base::test::TaskEnvironment task_environment_{
base::test::TaskEnvironment::TimeSource::MOCK_TIME};
void AddUser(const char* name, const char* password) {
std::string obfuscated =
brillo::cryptohome::home::SanitizeUserNameWithSalt(name, system_salt_);
brillo::SecureBlob passkey;
cryptohome::Crypto::PasswordToPasskey(password, system_salt_, &passkey);
Credentials credentials(name, passkey);
UserInfo info = {name,
obfuscated,
passkey,
credentials,
ShadowRoot().Append(obfuscated),
brillo::cryptohome::home::GetHashedUserPath(obfuscated)};
users_.push_back(info);
}
void PrepareDirectoryStructure() {
ASSERT_TRUE(platform_.CreateDirectory(ShadowRoot()));
ASSERT_TRUE(platform_.CreateDirectory(
brillo::cryptohome::home::GetUserPathPrefix()));
}
void PreparePolicy(bool enterprise_owned, const std::string& owner) {
homedirs_->set_enterprise_owned(enterprise_owned);
EXPECT_CALL(*mock_device_policy_, LoadPolicy())
.WillRepeatedly(Return(true));
EXPECT_CALL(*mock_device_policy_, GetOwner(_))
.WillRepeatedly(DoAll(SetArgPointee<0>(owner), Return(!owner.empty())));
}
};
MATCHER_P(VaultOptionsEqual, options, "") {
return memcmp(&options, &arg, sizeof(options)) == 0;
}
// Mount twice: first time with create, and the second time for the existing
// one.
TEST_F(UserSessionTest, MountVaultOk) {
// SETUP
const base::Time kTs1 = base::Time::FromInternalValue(42);
const base::Time kTs2 = base::Time::FromInternalValue(43);
const base::Time kTs3 = base::Time::FromInternalValue(44);
// Test with ecryptfs since it has a simpler existence check.
CryptohomeVault::Options options = {
.force_type = EncryptedContainerType::kEcryptfs,
};
// Set the credentials with |users_[0].credentials| so that
// |obfuscated_username_| is explicitly set during the Unmount test.
session_->SetCredentials(users_[0].credentials);
std::unique_ptr<VaultKeyset> vk = keyset_management_->GetValidKeyset(
users_[0].credentials, nullptr /*error*/);
FileSystemKeyset fs_keyset(*vk.get());
EXPECT_CALL(*mount_,
MountCryptohome(users_[0].name, _, VaultOptionsEqual(options)))
.WillOnce(Return(MOUNT_ERROR_NONE));
EXPECT_CALL(platform_, GetCurrentTime()).WillOnce(Return(kTs1));
// TEST
EXPECT_EQ(MOUNT_ERROR_NONE,
session_->MountVault(users_[0].name, fs_keyset, options));
// VERIFY
// Vault created.
EXPECT_THAT(user_activity_timestamp_manager_->GetLastUserActivityTimestamp(
users_[0].obfuscated),
Eq(kTs1));
EXPECT_NE(session_->GetWebAuthnSecret(), nullptr);
EXPECT_FALSE(session_->GetWebAuthnSecretHash().empty());
EXPECT_NE(session_->GetPkcs11Token(), nullptr);
ASSERT_FALSE(session_->GetPkcs11Token()->IsReady());
ASSERT_TRUE(session_->GetPkcs11Token()->Insert());
ASSERT_TRUE(session_->GetPkcs11Token()->IsReady());
// SETUP
// TODO(dlunev): this is required to mimic a real Mount::PrepareCryptohome
// call. Remove it when we are not mocking mount.
platform_.CreateDirectory(GetEcryptfsUserVaultPath(users_[0].obfuscated));
// Mount args with no create.
options = {};
// Set the credentials with |users_[0].credentials| so that
// |obfuscated_username_| is explicitly set during the Unmount test.
session_->SetCredentials(users_[0].credentials);
EXPECT_CALL(*mount_,
MountCryptohome(users_[0].name, _, VaultOptionsEqual(options)))
.WillOnce(Return(MOUNT_ERROR_NONE));
EXPECT_CALL(platform_, GetCurrentTime()).WillOnce(Return(kTs2));
// TEST
EXPECT_EQ(MOUNT_ERROR_NONE,
session_->MountVault(users_[0].name, fs_keyset, options));
// VERIFY
// Vault still exists when tried to remount with no create.
// ts updated on mount
EXPECT_TRUE(platform_.DirectoryExists(users_[0].homedir_path));
EXPECT_THAT(user_activity_timestamp_manager_->GetLastUserActivityTimestamp(
users_[0].obfuscated),
Eq(kTs2));
ASSERT_FALSE(session_->GetPkcs11Token()->IsReady());
ASSERT_TRUE(session_->GetPkcs11Token()->Insert());
ASSERT_TRUE(session_->GetPkcs11Token()->IsReady());
// SETUP
// Set the credentials with |users_[0].credentials| so that
// |obfuscated_username_| is explicitly set during the Unmount test.
session_->SetCredentials(users_[0].credentials);
EXPECT_CALL(*mount_, IsNonEphemeralMounted()).WillOnce(Return(true));
EXPECT_CALL(platform_, GetCurrentTime()).WillOnce(Return(kTs3));
EXPECT_CALL(*mount_, UnmountCryptohome()).WillOnce(Return(true));
// TEST
ASSERT_TRUE(session_->Unmount());
// VERIFY
// ts updated on unmount
EXPECT_THAT(user_activity_timestamp_manager_->GetLastUserActivityTimestamp(
users_[0].obfuscated),
Eq(kTs3));
EXPECT_EQ(session_->GetPkcs11Token(), nullptr);
}
TEST_F(UserSessionTest, EphemeralMountPolicyTest) {
EXPECT_CALL(*mount_, MountEphemeralCryptohome(_))
.WillRepeatedly(Return(MOUNT_ERROR_NONE));
struct PolicyTestCase {
std::string name;
bool is_enterprise;
std::string owner;
std::string user;
MountError expected_result;
};
std::vector<PolicyTestCase> test_cases{
{
.name = "NotEnterprise_NoOwner_UserLogin_OK",
.is_enterprise = false,
.owner = "",
.user = "some_user",
.expected_result = MOUNT_ERROR_EPHEMERAL_MOUNT_BY_OWNER,
},
{
.name = "NotEnterprise_Owner_UserLogin_OK",
.is_enterprise = false,
.owner = "owner",
.user = "some_user",
.expected_result = MOUNT_ERROR_NONE,
},
{
.name = "NotEnterprise_Owner_OwnerLogin_Error",
.is_enterprise = false,
.owner = "owner",
.user = "owner",
.expected_result = MOUNT_ERROR_EPHEMERAL_MOUNT_BY_OWNER,
},
{
.name = "Enterprise_NoOwner_UserLogin_OK",
.is_enterprise = true,
.owner = "",
.user = "some_user",
.expected_result = MOUNT_ERROR_NONE,
},
{
.name = "Enterprise_Owner_UserLogin_OK",
.is_enterprise = true,
.owner = "owner",
.user = "some_user",
.expected_result = MOUNT_ERROR_NONE,
},
{
.name = "Enterprise_Owner_OwnerLogin_OK",
.is_enterprise = true,
.owner = "owner",
.user = "owner",
.expected_result = MOUNT_ERROR_NONE,
},
};
for (const auto& test_case : test_cases) {
PreparePolicy(test_case.is_enterprise, test_case.owner);
ASSERT_THAT(session_->MountEphemeral(test_case.user),
test_case.expected_result)
<< "Test case: " << test_case.name;
}
}
// WebAuthn secret is cleared after read once.
TEST_F(UserSessionTest, WebAuthnSecretReadTwice) {
// SETUP
// Test with ecryptfs since it has a simpler existence check.
CryptohomeVault::Options options = {
.force_type = EncryptedContainerType::kEcryptfs,
};
MountError code = MOUNT_ERROR_NONE;
std::unique_ptr<VaultKeyset> vk =
keyset_management_->GetValidKeyset(users_[0].credentials, &code);
EXPECT_EQ(code, MOUNT_ERROR_NONE);
EXPECT_NE(vk, nullptr);
FileSystemKeyset fs_keyset(*vk.get());
EXPECT_CALL(*mount_,
MountCryptohome(users_[0].name, _, VaultOptionsEqual(options)))
.WillOnce(Return(MOUNT_ERROR_NONE));
EXPECT_EQ(MOUNT_ERROR_NONE,
session_->MountVault(users_[0].name, fs_keyset, options));
const std::string message(kWebAuthnSecretHmacMessage);
auto expected_webauthn_secret = std::make_unique<brillo::SecureBlob>(
HmacSha256(brillo::SecureBlob::Combine(fs_keyset.Key().fnek,
fs_keyset.Key().fek),
brillo::Blob(message.cbegin(), message.cend())));
EXPECT_NE(expected_webauthn_secret, nullptr);
// TEST
std::unique_ptr<brillo::SecureBlob> actual_webauthn_secret =
session_->GetWebAuthnSecret();
EXPECT_NE(actual_webauthn_secret, nullptr);
EXPECT_EQ(*actual_webauthn_secret, *expected_webauthn_secret);
EXPECT_FALSE(session_->GetWebAuthnSecretHash().empty());
// VERIFY
// The second read should get nothing.
EXPECT_EQ(session_->GetWebAuthnSecret(), nullptr);
// The second read of the WebAuthn secret hash should still get the hash.
EXPECT_FALSE(session_->GetWebAuthnSecretHash().empty());
}
// WebAuthn secret is cleared after timeout.
TEST_F(UserSessionTest, WebAuthnSecretTimeout) {
// SETUP
// Test with ecryptfs since it has a simpler existence check.
CryptohomeVault::Options options = {
.force_type = EncryptedContainerType::kEcryptfs,
};
EXPECT_CALL(*mount_,
MountCryptohome(users_[0].name, _, VaultOptionsEqual(options)))
.WillOnce(Return(MOUNT_ERROR_NONE));
std::unique_ptr<VaultKeyset> vk = keyset_management_->GetValidKeyset(
users_[0].credentials, nullptr /*error*/);
EXPECT_EQ(MOUNT_ERROR_NONE,
session_->MountVault(users_[0].name, FileSystemKeyset(*vk.get()),
options));
// TEST
// TODO(b/184393647): Since GetWebAuthnSecret is not currently used yet,
// the timer for clearing WebAuthn secret for security is minimized. It will
// be set to appropriate duration after secret enforcement.
task_environment_.FastForwardBy(base::TimeDelta::FromSeconds(0));
// VERIFY
EXPECT_EQ(session_->GetWebAuthnSecret(), nullptr);
// The WebAuthn secret hash will not be cleared after timeout.
EXPECT_FALSE(session_->GetWebAuthnSecretHash().empty());
}
class UserSessionReAuthTest : public ::testing::Test {
public:
UserSessionReAuthTest() : salt() {}
virtual ~UserSessionReAuthTest() {}
// Not copyable or movable
UserSessionReAuthTest(const UserSessionReAuthTest&) = delete;
UserSessionReAuthTest& operator=(const UserSessionReAuthTest&) = delete;
UserSessionReAuthTest(UserSessionReAuthTest&&) = delete;
UserSessionReAuthTest& operator=(UserSessionReAuthTest&&) = delete;
void SetUp() {
salt.resize(16);
GetSecureRandom(salt.data(), salt.size());
}
protected:
SecureBlob salt;
};
TEST_F(UserSessionReAuthTest, VerifyUser) {
Credentials credentials("username", SecureBlob("password"));
scoped_refptr<UserSession> session =
new UserSession(nullptr, nullptr, nullptr, nullptr, salt, nullptr);
EXPECT_TRUE(session->SetCredentials(credentials));
EXPECT_TRUE(session->VerifyUser(credentials.GetObfuscatedUsername(salt)));
EXPECT_FALSE(session->VerifyUser("other"));
}
TEST_F(UserSessionReAuthTest, VerifyCredentials) {
Credentials credentials_1("username", SecureBlob("password"));
Credentials credentials_2("username", SecureBlob("password2"));
Credentials credentials_3("username2", SecureBlob("password2"));
scoped_refptr<UserSession> session =
new UserSession(nullptr, nullptr, nullptr, nullptr, salt, nullptr);
EXPECT_TRUE(session->SetCredentials(credentials_1));
EXPECT_TRUE(session->VerifyCredentials(credentials_1));
EXPECT_FALSE(session->VerifyCredentials(credentials_2));
EXPECT_FALSE(session->VerifyCredentials(credentials_3));
EXPECT_TRUE(session->SetCredentials(credentials_2));
EXPECT_FALSE(session->VerifyCredentials(credentials_1));
EXPECT_TRUE(session->VerifyCredentials(credentials_2));
EXPECT_FALSE(session->VerifyCredentials(credentials_3));
EXPECT_TRUE(session->SetCredentials(credentials_3));
EXPECT_FALSE(session->VerifyCredentials(credentials_1));
EXPECT_FALSE(session->VerifyCredentials(credentials_2));
EXPECT_TRUE(session->VerifyCredentials(credentials_3));
}
} // namespace cryptohome