| // Copyright 2021 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "cryptohome/auth_session_manager.h" |
| |
| #include <array> |
| #include <memory> |
| #include <string> |
| #include <utility> |
| #include <vector> |
| |
| #include <base/functional/bind.h> |
| #include <base/functional/callback_forward.h> |
| #include <base/test/bind.h> |
| #include <base/test/power_monitor_test.h> |
| #include <base/test/task_environment.h> |
| #include <base/test/test_future.h> |
| #include <base/time/time.h> |
| #include <base/unguessable_token.h> |
| #include <brillo/cryptohome.h> |
| #include <cryptohome/proto_bindings/UserDataAuth.pb.h> |
| #include <gmock/gmock.h> |
| #include <gtest/gtest.h> |
| #include <libhwsec/frontend/cryptohome/mock_frontend.h> |
| #include <libhwsec/frontend/pinweaver_manager/mock_frontend.h> |
| #include <libhwsec-foundation/error/testing_helper.h> |
| #include <libstorage/platform/mock_platform.h> |
| |
| #include "cryptohome/auth_blocks/mock_auth_block_utility.h" |
| #include "cryptohome/auth_factor/manager.h" |
| #include "cryptohome/fake_features.h" |
| #include "cryptohome/fake_platform.h" |
| #include "cryptohome/mock_cryptohome_keys_manager.h" |
| #include "cryptohome/mock_keyset_management.h" |
| #include "cryptohome/mock_signalling.h" |
| #include "cryptohome/user_secret_stash/manager.h" |
| #include "cryptohome/user_secret_stash/storage.h" |
| #include "cryptohome/user_session/user_session_map.h" |
| |
| namespace cryptohome { |
| namespace { |
| |
| using ::base::test::TaskEnvironment; |
| using ::base::test::TestFuture; |
| using ::brillo::cryptohome::home::SanitizeUserName; |
| using ::hwsec_foundation::error::testing::IsOk; |
| using ::hwsec_foundation::error::testing::NotOk; |
| using ::testing::_; |
| using ::testing::AllOf; |
| using ::testing::ByMove; |
| using ::testing::ElementsAre; |
| using ::testing::Eq; |
| using ::testing::Gt; |
| using ::testing::IsEmpty; |
| using ::testing::IsNull; |
| using ::testing::IsTrue; |
| using ::testing::Le; |
| using ::testing::NiceMock; |
| using ::testing::NotNull; |
| using ::testing::Return; |
| using ::testing::SaveArg; |
| using ::testing::UnorderedElementsAre; |
| |
| class AuthSessionManagerTest : public ::testing::Test { |
| public: |
| AuthSessionManagerTest() : platform_(std::make_unique<FakePlatform>()) {} |
| |
| protected: |
| // Helper function that will try and "take" control of an auth session in a |
| // synchronous manner. If the session is in use then it will immediately |
| // return null. |
| template <typename T> |
| std::optional<InUseAuthSession> TryTakeAuthSession(const T& token) { |
| TestFuture<InUseAuthSession> session_future; |
| auth_session_manager_.RunWhenAvailable(token, session_future.GetCallback()); |
| if (session_future.IsReady()) { |
| return session_future.Take(); |
| } |
| return std::nullopt; |
| } |
| |
| // Version of TryTake that assumes that the session is available and |
| // CHECK-fails if it is not. This makes code easier to read but you should |
| // only use it in tests where it is easy to see that the session is not |
| // already in use. Ideally we'd use ASSERT instead of CHECK but that does not |
| // work with functions that do not return void. |
| template <typename T> |
| InUseAuthSession TakeAuthSession(const T& token) { |
| std::optional<InUseAuthSession> session = TryTakeAuthSession(token); |
| CHECK(session.has_value()); |
| return std::move(*session); |
| } |
| |
| const Username kUsername{"foo@example.com"}; |
| const Username kUsername2{"bar@example.com"}; |
| |
| base::test::ScopedPowerMonitorTestSource test_power_monitor_; |
| TaskEnvironment task_environment_{ |
| TaskEnvironment::TimeSource::MOCK_TIME, |
| TaskEnvironment::ThreadPoolExecutionMode::QUEUED}; |
| |
| NiceMock<libstorage::MockPlatform> platform_; |
| NiceMock<hwsec::MockCryptohomeFrontend> hwsec_; |
| NiceMock<hwsec::MockPinWeaverManagerFrontend> hwsec_pw_manager_; |
| NiceMock<MockCryptohomeKeysManager> cryptohome_keys_manager_; |
| Crypto crypto_{&hwsec_, &hwsec_pw_manager_, &cryptohome_keys_manager_, |
| nullptr}; |
| UssStorage uss_storage_{&platform_}; |
| UssManager uss_manager_{uss_storage_}; |
| UserSessionMap user_session_map_; |
| NiceMock<MockKeysetManagement> keyset_management_; |
| NiceMock<MockAuthBlockUtility> auth_block_utility_; |
| std::unique_ptr<FingerprintAuthBlockService> fp_service_{ |
| FingerprintAuthBlockService::MakeNullService()}; |
| AuthFactorDriverManager auth_factor_driver_manager_{ |
| &platform_, |
| &crypto_, |
| &uss_manager_, |
| AsyncInitPtr<ChallengeCredentialsHelper>(nullptr), |
| nullptr, |
| fp_service_.get(), |
| AsyncInitPtr<BiometricsAuthBlockService>(nullptr)}; |
| AuthFactorManager auth_factor_manager_{&platform_, &keyset_management_, |
| &uss_manager_}; |
| FakeFeaturesForTesting features_; |
| NiceMock<MockSignalling> signalling_; |
| AuthSession::BackingApis backing_apis_{ |
| &crypto_, |
| &platform_, |
| &user_session_map_, |
| &keyset_management_, |
| &auth_block_utility_, |
| &auth_factor_driver_manager_, |
| &auth_factor_manager_, |
| &uss_storage_, |
| &uss_manager_, |
| &features_.async, |
| AsyncInitPtr<SignallingInterface>(&signalling_)}; |
| AuthSessionManager auth_session_manager_{backing_apis_}; |
| }; |
| |
| TEST_F(AuthSessionManagerTest, CreateRemove) { |
| base::UnguessableToken token = auth_session_manager_.CreateAuthSession( |
| kUsername, 0, AuthIntent::kDecrypt); |
| |
| // After InUseAuthSession is freed, then AuthSessionManager can operate on the |
| // token and remove it. |
| EXPECT_TRUE(auth_session_manager_.RemoveAuthSession(token)); |
| InUseAuthSession in_use_auth_session = TakeAuthSession(token); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), NotOk()); |
| |
| // Repeat with serialized_token overload. |
| token = auth_session_manager_.CreateAuthSession(kUsername, 0, |
| AuthIntent::kDecrypt); |
| std::string serialized_token = |
| *AuthSession::GetSerializedStringFromToken(token); |
| |
| // Should succeed now that AuthSessionManager owns the AuthSession. |
| EXPECT_TRUE(auth_session_manager_.RemoveAuthSession(serialized_token)); |
| in_use_auth_session = TakeAuthSession(serialized_token); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), NotOk()); |
| } |
| |
| TEST_F(AuthSessionManagerTest, CreateExpire) { |
| base::UnguessableToken tokens[2]; |
| |
| // Create a pair of auth sessions. Before they're authenticated they should |
| // have infinite time remaining. |
| tokens[0] = auth_session_manager_.CreateAuthSession(kUsername, 0, |
| AuthIntent::kDecrypt); |
| tokens[1] = auth_session_manager_.CreateAuthSession(kUsername2, 0, |
| AuthIntent::kDecrypt); |
| for (const auto& token : tokens) { |
| InUseAuthSession in_use_auth_session = TakeAuthSession(token); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT(in_use_auth_session.GetRemainingTime().is_max(), IsTrue()); |
| } |
| |
| // Authenticate the sessions. Theys should now have finite timeouts. |
| for (auto& token : tokens) { |
| InUseAuthSession in_use_auth_session = TakeAuthSession(token); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT(in_use_auth_session->OnUserCreated(), IsOk()); |
| EXPECT_THAT( |
| in_use_auth_session->authorized_intents(), |
| UnorderedElementsAre(AuthIntent::kDecrypt, AuthIntent::kVerifyOnly)); |
| } |
| for (const auto& token : tokens) { |
| InUseAuthSession in_use_auth_session = TakeAuthSession(token); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT( |
| in_use_auth_session.GetRemainingTime(), |
| AllOf(Gt(base::TimeDelta()), Le(AuthSessionManager::kAuthTimeout))); |
| } |
| |
| EXPECT_CALL(signalling_, SendAuthSessionExpiring(_)).Times(2); |
| // Advance the clock by timeout. This should expire all the sessions. |
| task_environment_.FastForwardBy(AuthSessionManager::kAuthTimeout); |
| |
| // After expiration the sessions should be gone. |
| for (const auto& token : tokens) { |
| InUseAuthSession in_use_auth_session = TakeAuthSession(token); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), NotOk()); |
| } |
| } |
| |
| TEST_F(AuthSessionManagerTest, ExtendExpire) { |
| base::UnguessableToken tokens[2]; |
| |
| // Create and set up a pair of auth sessions, setting them to authenticated so |
| // that they can eventually get expired. |
| tokens[0] = auth_session_manager_.CreateAuthSession(kUsername, 0, |
| AuthIntent::kDecrypt); |
| tokens[1] = auth_session_manager_.CreateAuthSession(kUsername2, 0, |
| AuthIntent::kDecrypt); |
| for (auto& token : tokens) { |
| InUseAuthSession auth_session = TakeAuthSession(token); |
| ASSERT_THAT(auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT(auth_session->OnUserCreated(), IsOk()); |
| EXPECT_THAT( |
| auth_session->authorized_intents(), |
| UnorderedElementsAre(AuthIntent::kDecrypt, AuthIntent::kVerifyOnly)); |
| } |
| |
| // Before expiration we should be able to look up the sessions again. |
| for (const auto& token : tokens) { |
| InUseAuthSession in_use_auth_session = TakeAuthSession(token); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT( |
| in_use_auth_session.GetRemainingTime(), |
| AllOf(Gt(base::TimeDelta()), Le(AuthSessionManager::kAuthTimeout))); |
| } |
| |
| // Extend the first session to seven minutes. |
| { |
| InUseAuthSession in_use_auth_session = TakeAuthSession(tokens[0]); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT(in_use_auth_session.ExtendTimeout( |
| AuthSessionManager::kAuthTimeout + base::Minutes(2)), |
| IsOk()); |
| EXPECT_THAT(in_use_auth_session.GetRemainingTime(), |
| AllOf(Gt(base::TimeDelta()), |
| Le(AuthSessionManager::kAuthTimeout + base::Minutes(2)))); |
| } |
| |
| // Extend the second session to two minutes (this is a no-op) |
| { |
| InUseAuthSession in_use_auth_session = TakeAuthSession(tokens[1]); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT(in_use_auth_session.ExtendTimeout(base::Minutes(2)), IsOk()); |
| EXPECT_THAT( |
| in_use_auth_session.GetRemainingTime(), |
| AllOf(Gt(base::TimeDelta()), Le(AuthSessionManager::kAuthTimeout))); |
| } |
| |
| // Move the time forward by timeout plus one minute. |
| task_environment_.FastForwardBy(base::Minutes(2)); |
| |
| // Both Session should be good be good. |
| { |
| InUseAuthSession in_use_auth_session = TakeAuthSession(tokens[0]); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT(in_use_auth_session.GetRemainingTime(), |
| AllOf(Gt(base::TimeDelta()), Le(base::Minutes(5)))); |
| } |
| { |
| InUseAuthSession in_use_auth_session = TakeAuthSession(tokens[1]); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT(in_use_auth_session.GetRemainingTime(), |
| AllOf(Gt(base::TimeDelta()), Le(base::Minutes(3)))); |
| } |
| |
| // Move the time forward by timeout by another four minutes. This should |
| // timeout the second session (original timeout) but not the first (added two |
| // minutes). |
| { |
| user_data_auth::AuthSessionExpiring signal_proto; |
| EXPECT_CALL(signalling_, SendAuthSessionExpiring(_)) |
| .WillOnce(SaveArg<0>(&signal_proto)); |
| |
| task_environment_.FastForwardBy(base::Minutes(3)); |
| InUseAuthSession in_use_auth_session = TakeAuthSession(tokens[0]); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT(in_use_auth_session.GetRemainingTime(), |
| AllOf(Gt(base::TimeDelta()), Le(base::Minutes(2)))); |
| EXPECT_THAT(signal_proto.time_left(), |
| AllOf(testing::Ge(0), |
| Le(AuthSessionManager::kAuthTimeoutWarning.InSeconds()))); |
| } |
| { |
| InUseAuthSession in_use_auth_session = TakeAuthSession(tokens[1]); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), NotOk()); |
| } |
| |
| // Move time forward by another minute. This should move the session to |
| // expiring soon. AuthSession should still be good. |
| { |
| user_data_auth::AuthSessionExpiring signal_proto; |
| EXPECT_CALL(signalling_, SendAuthSessionExpiring(_)) |
| .WillOnce(SaveArg<0>(&signal_proto)); |
| |
| task_environment_.FastForwardBy(base::Minutes(1)); |
| InUseAuthSession in_use_auth_session = TakeAuthSession(tokens[0]); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT(in_use_auth_session.GetRemainingTime(), |
| AllOf(Gt(base::TimeDelta()), Le(base::Minutes(1)))); |
| EXPECT_THAT(signal_proto.time_left(), |
| AllOf(testing::Ge(0), |
| Le(AuthSessionManager::kAuthTimeoutWarning.InSeconds()))); |
| } |
| |
| // Extend the first session to two minutes. |
| { |
| InUseAuthSession in_use_auth_session = TakeAuthSession(tokens[0]); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT(in_use_auth_session.ExtendTimeout(base::Minutes(2)), IsOk()); |
| EXPECT_THAT(in_use_auth_session.GetRemainingTime(), |
| AllOf(Gt(base::TimeDelta()), Le(base::Seconds(150)))); |
| } |
| |
| // Move time forward by another minute. This should kill the last session. |
| EXPECT_CALL(signalling_, SendAuthSessionExpiring(_)); |
| task_environment_.FastForwardBy(base::Minutes(3)); |
| |
| // Now both sessions should be gone. |
| for (const auto& token : tokens) { |
| InUseAuthSession in_use_auth_session = TakeAuthSession(token); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), NotOk()); |
| } |
| } |
| |
| TEST_F(AuthSessionManagerTest, CreateExpireAfterPowerSuspend) { |
| // Create and authenticate a session. |
| base::UnguessableToken token = auth_session_manager_.CreateAuthSession( |
| kUsername, 0, AuthIntent::kDecrypt); |
| { |
| InUseAuthSession auth_session = TakeAuthSession(token); |
| ASSERT_THAT(auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT(auth_session->OnUserCreated(), IsOk()); |
| EXPECT_THAT( |
| auth_session->authorized_intents(), |
| UnorderedElementsAre(AuthIntent::kDecrypt, AuthIntent::kVerifyOnly)); |
| EXPECT_THAT( |
| auth_session.GetRemainingTime(), |
| AllOf(Gt(base::TimeDelta()), Le(AuthSessionManager::kAuthTimeout))); |
| } |
| |
| // Have the device power off for 30 seconds |
| constexpr auto time_passed = base::Seconds(30); |
| test_power_monitor_.Suspend(); |
| task_environment_.SuspendedFastForwardBy(time_passed); |
| test_power_monitor_.Resume(); |
| { |
| InUseAuthSession auth_session = TakeAuthSession(token); |
| ASSERT_THAT(auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT(auth_session.GetRemainingTime(), |
| AllOf(Gt(base::TimeDelta()), |
| Le(AuthSessionManager::kAuthTimeout - time_passed))); |
| } |
| |
| // Advance the clock the rest of the way. |
| task_environment_.FastForwardBy(AuthSessionManager::kAuthTimeout - |
| time_passed); |
| |
| // After expiration the session should be gone. |
| { |
| InUseAuthSession auth_session = TakeAuthSession(token); |
| ASSERT_THAT(auth_session.AuthSessionStatus(), NotOk()); |
| } |
| } |
| |
| TEST_F(AuthSessionManagerTest, AddRemove) { |
| base::UnguessableToken token = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| |
| // After InUseAuthSession is freed, then AuthSessionManager can operate on the |
| // token and remove it. |
| EXPECT_TRUE(auth_session_manager_.RemoveAuthSession(token)); |
| InUseAuthSession in_use_auth_session = TakeAuthSession(token); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), NotOk()); |
| |
| // Repeat with serialized_token overload. |
| token = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| std::string serialized_token = |
| *AuthSession::GetSerializedStringFromToken(token); |
| |
| // Should succeed now that AuthSessionManager owns the AuthSession. |
| EXPECT_TRUE(auth_session_manager_.RemoveAuthSession(serialized_token)); |
| in_use_auth_session = TakeAuthSession(serialized_token); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), NotOk()); |
| } |
| |
| TEST_F(AuthSessionManagerTest, AddRemoveExpringSoon) { |
| base::UnguessableToken token; |
| |
| // Create an auth session. |
| token = auth_session_manager_.CreateAuthSession(kUsername, 0, |
| AuthIntent::kDecrypt); |
| |
| // Authenticate the session. It should now have finite timeout. |
| { |
| InUseAuthSession in_use_auth_session = TakeAuthSession(token); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT(in_use_auth_session->OnUserCreated(), IsOk()); |
| EXPECT_THAT( |
| in_use_auth_session->authorized_intents(), |
| UnorderedElementsAre(AuthIntent::kDecrypt, AuthIntent::kVerifyOnly)); |
| } |
| |
| { |
| InUseAuthSession in_use_auth_session = TakeAuthSession(token); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), IsOk()); |
| EXPECT_THAT( |
| in_use_auth_session.GetRemainingTime(), |
| AllOf(Gt(base::TimeDelta()), Le(AuthSessionManager::kAuthTimeout))); |
| } |
| |
| // Ensure that the authsession moves to expiring soon map. |
| task_environment_.FastForwardBy(AuthSessionManager::kAuthTimeout - |
| base::Seconds(59)); |
| |
| EXPECT_TRUE(auth_session_manager_.RemoveAuthSession(token)); |
| // Ensure that expiring soon map's callbacks are called. |
| task_environment_.FastForwardBy(base::Seconds(60)); |
| } |
| |
| TEST_F(AuthSessionManagerTest, AddRemoveUser) { |
| base::UnguessableToken u1_token = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| base::UnguessableToken u2_token = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername2, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| |
| // We should be able to grab sessions for both tokens. |
| { |
| InUseAuthSession u1_session = TakeAuthSession(u1_token); |
| InUseAuthSession u2_session = TakeAuthSession(u2_token); |
| ASSERT_THAT(u1_session.AuthSessionStatus(), IsOk()); |
| ASSERT_THAT(u2_session.AuthSessionStatus(), IsOk()); |
| } |
| |
| // Now remove all sessions for user 2. We should only be able to grab the |
| // session for u1 after this. |
| auth_session_manager_.RemoveUserAuthSessions(SanitizeUserName(kUsername2)); |
| { |
| InUseAuthSession u1_session = TakeAuthSession(u1_token); |
| InUseAuthSession u2_session = TakeAuthSession(u2_token); |
| ASSERT_THAT(u1_session.AuthSessionStatus(), IsOk()); |
| ASSERT_THAT(u2_session.AuthSessionStatus(), NotOk()); |
| } |
| |
| // Do the same for user 1. |
| auth_session_manager_.RemoveUserAuthSessions(SanitizeUserName(kUsername)); |
| { |
| InUseAuthSession u1_session = TakeAuthSession(u1_token); |
| InUseAuthSession u2_session = TakeAuthSession(u2_token); |
| ASSERT_THAT(u1_session.AuthSessionStatus(), NotOk()); |
| ASSERT_THAT(u2_session.AuthSessionStatus(), NotOk()); |
| } |
| } |
| |
| TEST_F(AuthSessionManagerTest, AddAndWaitRemove) { |
| base::UnguessableToken token; |
| bool is_called = false; |
| InUseAuthSession saved_session; |
| auto callback = [&saved_session, &is_called](InUseAuthSession auth_session) { |
| is_called = true; |
| saved_session = std::move(auth_session); |
| }; |
| TestFuture<InUseAuthSession> future; |
| |
| // Start scope for first InUseAuthSession |
| { |
| token = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| TestFuture<InUseAuthSession> created_future; |
| auth_session_manager_.RunWhenAvailable(token, created_future.GetCallback()); |
| InUseAuthSession auth_session = created_future.Take(); |
| ASSERT_THAT(auth_session.AuthSessionStatus(), IsOk()); |
| |
| // RunWhenAvailable on the same token will not trigger the callback |
| // directly, but wait for the session is not in use instead. |
| auth_session_manager_.RunWhenAvailable( |
| token, base::BindLambdaForTesting(callback)); |
| EXPECT_FALSE(is_called); |
| |
| // |future| will be queued behind |callback|. |
| auth_session_manager_.RunWhenAvailable(token, future.GetCallback()); |
| EXPECT_FALSE(future.IsReady()); |
| |
| // Scope ends here to free the InUseAuthSession, after this |future| will |
| // be executed. |
| } |
| |
| ASSERT_TRUE(is_called); |
| EXPECT_THAT(saved_session.AuthSessionStatus(), IsOk()); |
| EXPECT_FALSE(future.IsReady()); |
| |
| // If we remove the token now, the callback is still not called until we |
| // release the ongoing session. |
| EXPECT_TRUE(auth_session_manager_.RemoveAuthSession(token)); |
| EXPECT_FALSE(future.IsReady()); |
| |
| // Release the existing in-use instance. The callback should now happen with |
| // an invalid session. |
| std::move(saved_session).Release(); |
| EXPECT_TRUE(future.IsReady()); |
| EXPECT_THAT(future.Get().AuthSessionStatus(), NotOk()); |
| } |
| |
| TEST_F(AuthSessionManagerTest, MultiUserBlocking) { |
| // Four session tokens. The first two tokens are sessions for user 1, and the |
| // other two for user 2. |
| std::array<base::UnguessableToken, 4> tokens; |
| |
| // Create two sessions each for two users. |
| tokens[0] = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| tokens[1] = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| tokens[2] = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername2, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| tokens[3] = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername2, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| |
| // Take ownership of a session for the first user. Work should be blocked on |
| // both sessions for that user, but runnable on the second user's sessions. |
| { |
| std::vector<size_t> work_done; |
| { |
| InUseAuthSession u1_session = TakeAuthSession(tokens[0]); |
| ASSERT_THAT(u1_session.AuthSessionStatus(), IsOk()); |
| |
| // Try to schedule work on every session. |
| for (size_t i = 0; i < tokens.size(); ++i) { |
| auth_session_manager_.RunWhenAvailable( |
| tokens[i], |
| base::BindLambdaForTesting( |
| [&work_done, i](InUseAuthSession) { work_done.push_back(i); })); |
| } |
| |
| // Check that the expected work was blocked (or not). |
| EXPECT_THAT(work_done, ElementsAre(2, 3)); |
| |
| // Scope ends here to free the InUseAuthSession, after this all the |
| // remaining work should get run. |
| } |
| EXPECT_THAT(work_done, ElementsAre(2, 3, 0, 1)); |
| } |
| |
| // Run the same test, but now with a session from the second user being held. |
| { |
| std::vector<size_t> work_done; |
| { |
| InUseAuthSession u2_session = TakeAuthSession(tokens[2]); |
| ASSERT_THAT(u2_session.AuthSessionStatus(), IsOk()); |
| |
| // Try to schedule work on every session. |
| for (size_t i = 0; i < tokens.size(); ++i) { |
| auth_session_manager_.RunWhenAvailable( |
| tokens[i], |
| base::BindLambdaForTesting( |
| [&work_done, i](InUseAuthSession) { work_done.push_back(i); })); |
| } |
| |
| // Check that the expected work was blocked (or not). |
| EXPECT_THAT(work_done, ElementsAre(0, 1)); |
| |
| // Scope ends here to free the InUseAuthSession, after this all the |
| // remaining work should get run. |
| } |
| EXPECT_THAT(work_done, ElementsAre(0, 1, 2, 3)); |
| } |
| |
| // Run the same test but hold sessions for both users. |
| { |
| std::vector<size_t> work_done; |
| { |
| InUseAuthSession u1_session = TakeAuthSession(tokens[1]); |
| ASSERT_THAT(u1_session.AuthSessionStatus(), IsOk()); |
| InUseAuthSession u2_session = TakeAuthSession(tokens[3]); |
| ASSERT_THAT(u2_session.AuthSessionStatus(), IsOk()); |
| |
| // Try to schedule work on every session. |
| for (size_t i = 0; i < tokens.size(); ++i) { |
| auth_session_manager_.RunWhenAvailable( |
| tokens[i], |
| base::BindLambdaForTesting( |
| [&work_done, i](InUseAuthSession) { work_done.push_back(i); })); |
| } |
| |
| // Check that all of the work was blocked. |
| EXPECT_THAT(work_done, IsEmpty()); |
| |
| // Scope ends here to free the sessions, all the work should execute. Note |
| // that the session for user 2 should be ended first. |
| } |
| EXPECT_THAT(work_done, ElementsAre(2, 3, 0, 1)); |
| } |
| } |
| |
| TEST_F(AuthSessionManagerTest, PendingWorkStaysBlockedAfterRemove) { |
| // Session tokens for two sessions for a user. |
| std::array<base::UnguessableToken, 2> tokens; |
| tokens[0] = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| tokens[1] = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| |
| // Track when work was done, and when work was done with a valid session. |
| std::vector<size_t> work_done, work_done_with_session; |
| { |
| InUseAuthSession session = TakeAuthSession(tokens[1]); |
| ASSERT_THAT(session.AuthSessionStatus(), IsOk()); |
| |
| // Try to schedule work alternating between both sessions. All of this work |
| // should be blocked because we're holding the second session. |
| for (size_t i = 0; i < 4 * tokens.size(); ++i) { |
| auth_session_manager_.RunWhenAvailable( |
| tokens[i % 2], |
| base::BindLambdaForTesting([&work_done, &work_done_with_session, |
| i](InUseAuthSession in_use_session) { |
| work_done.push_back(i); |
| if (in_use_session.AuthSessionStatus().ok()) { |
| work_done_with_session.push_back(i); |
| } |
| })); |
| } |
| EXPECT_THAT(work_done, IsEmpty()); |
| EXPECT_THAT(work_done_with_session, IsEmpty()); |
| |
| // Remove the session we're using. This should NOT unblock anything. |
| EXPECT_THAT(auth_session_manager_.RemoveAuthSession(session->token()), |
| IsTrue()); |
| EXPECT_THAT(work_done, IsEmpty()); |
| EXPECT_THAT(work_done_with_session, IsEmpty()); |
| |
| // Scope ends here to free the InUseAuthSession, after this all the |
| // remaining work should get run. However, only the work on the first |
| // session should be given a valid session to work with. |
| } |
| EXPECT_THAT(work_done, ElementsAre(0, 1, 2, 3, 4, 5, 6, 7)); |
| EXPECT_THAT(work_done_with_session, ElementsAre(0, 2, 4, 6)); |
| } |
| |
| TEST_F(AuthSessionManagerTest, RemovedSessionsStillBlockNewWork) { |
| // Session tokens for two sessions for a user. |
| std::array<base::UnguessableToken, 2> tokens; |
| tokens[0] = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| tokens[1] = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| |
| // Track when work was done, and when work was done with a valid session. |
| std::vector<size_t> work_done, work_done_with_session; |
| { |
| InUseAuthSession session = TakeAuthSession(tokens[1]); |
| ASSERT_THAT(session.AuthSessionStatus(), IsOk()); |
| |
| // Remove the session. It should still stay in use and block any work that |
| // we try to schedule for this user. |
| EXPECT_THAT(auth_session_manager_.RemoveAuthSession(session->token()), |
| IsTrue()); |
| |
| // Try to schedule work alternating between both sessions. Even though the |
| // second session has been removed it's still in use and so should still |
| // block any work against the first session. |
| for (size_t i = 0; i < 4 * tokens.size(); ++i) { |
| auth_session_manager_.RunWhenAvailable( |
| tokens[i % 2], |
| base::BindLambdaForTesting([&work_done, &work_done_with_session, |
| i](InUseAuthSession in_use_session) { |
| work_done.push_back(i); |
| if (in_use_session.AuthSessionStatus().ok()) { |
| work_done_with_session.push_back(i); |
| } |
| })); |
| } |
| EXPECT_THAT(work_done, ElementsAre(1, 3, 5, 7)); |
| EXPECT_THAT(work_done_with_session, IsEmpty()); |
| |
| // Scope ends here to free the InUseAuthSession, after this all the |
| // remaining work should get run. However, only the work on the first |
| // session should be given a valid session to work with. |
| } |
| EXPECT_THAT(work_done, ElementsAre(1, 3, 5, 7, 0, 2, 4, 6)); |
| EXPECT_THAT(work_done_with_session, ElementsAre(0, 2, 4, 6)); |
| } |
| |
| TEST_F(AuthSessionManagerTest, RemoveNonExisting) { |
| EXPECT_FALSE( |
| auth_session_manager_.RemoveAuthSession(base::UnguessableToken())); |
| EXPECT_FALSE(auth_session_manager_.RemoveAuthSession("non-existing-token")); |
| } |
| |
| TEST_F(AuthSessionManagerTest, FlagPassing) { |
| // Arrange. |
| base::UnguessableToken session_token = |
| auth_session_manager_.CreateAuthSession(kUsername, 0, |
| AuthIntent::kDecrypt); |
| InUseAuthSession auth_session = TakeAuthSession(session_token); |
| base::UnguessableToken ephemeral_session_token = |
| auth_session_manager_.CreateAuthSession( |
| kUsername2, user_data_auth::AUTH_SESSION_FLAGS_EPHEMERAL_USER, |
| AuthIntent::kDecrypt); |
| InUseAuthSession ephemeral_auth_session = |
| TakeAuthSession(ephemeral_session_token); |
| |
| // Assert |
| EXPECT_FALSE(auth_session->ephemeral_user()); |
| EXPECT_TRUE(ephemeral_auth_session->ephemeral_user()); |
| } |
| |
| TEST_F(AuthSessionManagerTest, IntentPassing) { |
| // Arrange. |
| base::UnguessableToken decryption_session_token = |
| auth_session_manager_.CreateAuthSession(kUsername, 0, |
| AuthIntent::kDecrypt); |
| InUseAuthSession decryption_auth_session = |
| TakeAuthSession(decryption_session_token); |
| base::UnguessableToken verification_session_token = |
| auth_session_manager_.CreateAuthSession(kUsername2, 0, |
| AuthIntent::kVerifyOnly); |
| InUseAuthSession verification_auth_session = |
| TakeAuthSession(verification_session_token); |
| |
| // Assert. |
| EXPECT_EQ(decryption_auth_session->auth_intent(), AuthIntent::kDecrypt); |
| EXPECT_EQ(verification_auth_session->auth_intent(), AuthIntent::kVerifyOnly); |
| } |
| |
| TEST_F(AuthSessionManagerTest, AddFindUnMount) { |
| // Start scope for first InUseAuthSession |
| base::UnguessableToken token = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| |
| // After InUseAuthSession is freed, then AuthSessionManager can operate on the |
| // token and remove it. |
| EXPECT_TRUE(auth_session_manager_.RemoveAuthSession(token)); |
| InUseAuthSession in_use_auth_session = TakeAuthSession(token); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), NotOk()); |
| |
| // Repeat with serialized_token overload. |
| token = auth_session_manager_.CreateAuthSession( |
| AuthSession::Params{.username = kUsername, |
| .is_ephemeral_user = false, |
| .intent = AuthIntent::kDecrypt, |
| .auth_factor_status_update_timer = |
| std::make_unique<base::WallClockTimer>(), |
| .user_exists = false}); |
| std::string serialized_token = |
| *AuthSession::GetSerializedStringFromToken(token); |
| |
| // Should succeed now that AuthSessionManager owns the AuthSession. |
| auth_session_manager_.RemoveAllAuthSessions(); |
| in_use_auth_session = TakeAuthSession(serialized_token); |
| ASSERT_THAT(in_use_auth_session.AuthSessionStatus(), NotOk()); |
| } |
| |
| // The timing on the bound session tests assumes that the short timeout evenly |
| // divides into the long timeout. The test will need to be adjusted if the |
| // constants are changed to violate that. |
| static_assert( |
| (BoundAuthSession::kTimeout % BoundAuthSession::kShortTimeout).is_zero(), |
| "The bound timeout is not an integer multiple of the short timeout"); |
| |
| TEST_F(AuthSessionManagerTest, BoundSessionExpiresIfBlocking) { |
| // Create a session and bind it. |
| base::UnguessableToken token = auth_session_manager_.CreateAuthSession( |
| kUsername, 0, AuthIntent::kDecrypt); |
| BoundAuthSession bound_session(TakeAuthSession(token)); |
| |
| // Schedule several tasks against the session, they should be blocked. |
| std::vector<size_t> work_done; |
| for (size_t i = 0; i < 4; ++i) { |
| auth_session_manager_.RunWhenAvailable( |
| token, base::BindLambdaForTesting( |
| [&work_done, i](InUseAuthSession in_use_session) { |
| work_done.push_back(i); |
| })); |
| } |
| EXPECT_THAT(work_done, IsEmpty()); |
| |
| // Advance the clock halfway to timeout. Everything should still be blocked. |
| task_environment_.FastForwardBy(BoundAuthSession::kTimeout / 2); |
| EXPECT_THAT(work_done, IsEmpty()); |
| |
| // Now advance it past the timeout. The bound session should time out. |
| task_environment_.FastForwardBy(BoundAuthSession::kTimeout / 2 + |
| BoundAuthSession::kShortTimeout / 2); |
| EXPECT_THAT(work_done, ElementsAre(0, 1, 2, 3)); |
| EXPECT_THAT(std::move(bound_session).Take().AuthSessionStatus(), NotOk()); |
| } |
| |
| TEST_F(AuthSessionManagerTest, BoundSessionDoesNotExpireIfNotBlocking) { |
| // Create a session and bind it. |
| base::UnguessableToken token = auth_session_manager_.CreateAuthSession( |
| kUsername, 0, AuthIntent::kDecrypt); |
| BoundAuthSession bound_session(TakeAuthSession(token)); |
| |
| // Advance the clock by many times the timeout interval. The session should |
| // still be bound because nothing is blocking it. |
| task_environment_.FastForwardBy(BoundAuthSession::kTimeout * 100); |
| EXPECT_THAT(std::move(bound_session).Take().AuthSessionStatus(), IsOk()); |
| } |
| |
| TEST_F(AuthSessionManagerTest, BoundSessionExpiresOnceWorkIsScheduled) { |
| // Create a session and bind it. |
| base::UnguessableToken token = auth_session_manager_.CreateAuthSession( |
| kUsername, 0, AuthIntent::kDecrypt); |
| BoundAuthSession bound_session(TakeAuthSession(token)); |
| |
| // Advance the clock by many times the timeout interval. The session should |
| // still be bound because nothing is blocking it. |
| task_environment_.FastForwardBy(BoundAuthSession::kTimeout * 100 + |
| BoundAuthSession::kShortTimeout / 2); |
| |
| // Schedule several tasks against the session, they should be blocked. |
| std::vector<size_t> work_done; |
| for (size_t i = 0; i < 4; ++i) { |
| auth_session_manager_.RunWhenAvailable( |
| token, base::BindLambdaForTesting( |
| [&work_done, i](InUseAuthSession in_use_session) { |
| work_done.push_back(i); |
| })); |
| } |
| EXPECT_THAT(work_done, IsEmpty()); |
| |
| // Advance the clock by a bit of the short timeout interval. Everything should |
| // still be blocked. |
| task_environment_.FastForwardBy(BoundAuthSession::kShortTimeout / 10); |
| EXPECT_THAT(work_done, IsEmpty()); |
| |
| // Advance the clock by the rest of the short timeout. The bound session |
| // should time out. |
| task_environment_.FastForwardBy(BoundAuthSession::kShortTimeout); |
| EXPECT_THAT(work_done, ElementsAre(0, 1, 2, 3)); |
| EXPECT_THAT(std::move(bound_session).Take().AuthSessionStatus(), NotOk()); |
| } |
| |
| } // namespace |
| } // namespace cryptohome |