diff options
author | TreeHugger Robot <treehugger-gerrit@google.com> | 2018-03-21 00:21:15 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2018-03-21 00:21:15 +0000 |
commit | 439e29d4a8258be6fd91b35344cbe2e060a06457 (patch) | |
tree | 776a9fd6edfbce131272954bfc7b766d6cef59fb | |
parent | 91de6db226d912249f42c406b7441bb7fb296850 (diff) | |
parent | 064ce8561211612a4bf08dd2134657ad855bc801 (diff) | |
download | security-439e29d4a8258be6fd91b35344cbe2e060a06457.tar.gz |
Merge "Implement rate limiting on a per app basis for confirmationui" into pi-dev
-rw-r--r-- | keystore/confirmation_manager.cpp | 8 | ||||
-rw-r--r-- | keystore/confirmation_manager.h | 3 | ||||
-rw-r--r-- | keystore/confirmationui_rate_limiting.h | 124 | ||||
-rw-r--r-- | keystore/tests/Android.bp | 2 | ||||
-rw-r--r-- | keystore/tests/confirmationui_rate_limiting_test.cpp | 274 |
5 files changed, 411 insertions, 0 deletions
diff --git a/keystore/confirmation_manager.cpp b/keystore/confirmation_manager.cpp index acca3045..0dee4aaa 100644 --- a/keystore/confirmation_manager.cpp +++ b/keystore/confirmation_manager.cpp @@ -23,6 +23,7 @@ #include <android/hardware/confirmationui/1.0/types.h> #include <android/security/BpConfirmationPromptCallback.h> #include <binder/BpBinder.h> +#include <binder/IPCThreadState.h> #include <binder/Parcel.h> #include "keystore_aidl_hidl_marshalling_utils.h" @@ -69,6 +70,12 @@ Status ConfirmationManager::presentConfirmationPrompt(const sp<IBinder>& listene return Status::ok(); } + uid_t callingUid = android::IPCThreadState::self()->getCallingUid(); + if (!mRateLimiting.tryPrompt(callingUid)) { + *aidl_return = static_cast<int32_t>(ConfirmationResponseCode::SystemError); + return Status::ok(); + } + String8 promptText8(promptText); String8 locale8(locale); vector<UIOption> uiOptionsVector; @@ -137,6 +144,7 @@ void ConfirmationManager::finalizeTransaction(ConfirmationResponseCode responseC // and b) ensure state has been cleared; before doing this... mMutex.lock(); + mRateLimiting.processResult(responseCode); sp<IBinder> listener = mCurrentListener; if (mCurrentListener != nullptr) { mCurrentListener->unlinkToDeath(mDeathRecipient); diff --git a/keystore/confirmation_manager.h b/keystore/confirmation_manager.h index b92dedaf..46b623cb 100644 --- a/keystore/confirmation_manager.h +++ b/keystore/confirmation_manager.h @@ -29,6 +29,8 @@ #include <utils/StrongPointer.h> #include <vector> +#include "confirmationui_rate_limiting.h" + namespace keystore { using android::binder::Status; @@ -94,6 +96,7 @@ class ConfirmationManager : public android::hardware::hidl_death_recipient, android::sp<android::hardware::confirmationui::V1_0::IConfirmationUI> mCurrentConfirmationUI; android::IBinder::DeathRecipient* mDeathRecipient; hidl_vec<uint8_t> mLatestConfirmationToken; + RateLimiting<> mRateLimiting; }; } // namespace keystore diff --git a/keystore/confirmationui_rate_limiting.h b/keystore/confirmationui_rate_limiting.h new file mode 100644 index 00000000..12c20fae --- /dev/null +++ b/keystore/confirmationui_rate_limiting.h @@ -0,0 +1,124 @@ +/* +** +** Copyright 2018, The Android Open Source Project +** +** Licensed under the Apache License, Version 2.0 (the "License"); +** you may not use this file except in compliance with the License. +** You may obtain a copy of the License at +** +** http://www.apache.org/licenses/LICENSE-2.0 +** +** Unless required by applicable law or agreed to in writing, software +** distributed under the License is distributed on an "AS IS" BASIS, +** WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +** See the License for the specific language governing permissions and +** limitations under the License. +*/ + +#ifndef KEYSTORE_CONFIRMATIONUI_RATE_LIMITING_H_ +#define KEYSTORE_CONFIRMATIONUI_RATE_LIMITING_H_ + +#include <android/hardware/confirmationui/1.0/types.h> +#include <chrono> +#include <stdint.h> +#include <sys/types.h> +#include <tuple> +#include <unordered_map> + +namespace keystore { + +using ConfirmationResponseCode = android::hardware::confirmationui::V1_0::ResponseCode; + +using std::chrono::time_point; +using std::chrono::duration; + +template <typename Clock = std::chrono::steady_clock> class RateLimiting { + private: + struct Slot { + Slot() : previous_start{}, prompt_start{}, counter(0) {} + typename Clock::time_point previous_start; + typename Clock::time_point prompt_start; + uint32_t counter; + }; + + std::unordered_map<uid_t, Slot> slots_; + + uint_t latest_requester_; + + static std::chrono::seconds getBackoff(uint32_t counter) { + using namespace std::chrono_literals; + switch (counter) { + case 0: + case 1: + case 2: + return 0s; + case 3: + case 4: + case 5: + return 30s; + default: + return 60s * (1ULL << (counter - 6)); + } + } + + public: + // Exposes the number of used slots. This is only used by the test to verify the assumption + // about used counter slots. + size_t usedSlots() const { return slots_.size(); } + void doGC() { + using namespace std::chrono_literals; + using std::chrono::system_clock; + using std::chrono::time_point_cast; + auto then = Clock::now() - 24h; + auto iter = slots_.begin(); + while (iter != slots_.end()) { + if (iter->second.prompt_start <= then) { + iter = slots_.erase(iter); + } else { + ++iter; + } + } + } + + bool tryPrompt(uid_t id) { + using namespace std::chrono_literals; + // remove slots that have not been touched in 24 hours + doGC(); + auto& slot = slots_[id]; + auto now = Clock::now(); + if (!slot.counter || slot.prompt_start <= now - getBackoff(slot.counter)) { + latest_requester_ = id; + slot.counter += 1; + slot.previous_start = slot.prompt_start; + slot.prompt_start = now; + return true; + } + return false; + } + + void processResult(ConfirmationResponseCode rc) { + switch (rc) { + case ConfirmationResponseCode::OK: + // reset the counter slot + slots_.erase(latest_requester_); + return; + case ConfirmationResponseCode::Canceled: + // nothing to do here + return; + default:; + } + + // roll back latest request + auto& slot = slots_[latest_requester_]; + if (slot.counter <= 1) { + slots_.erase(latest_requester_); + return; + } + slot.counter -= 1; + slot.prompt_start = slot.previous_start; + } +}; + +} // namespace keystore + +#endif // KEYSTORE_CONFIRMATIONUI_RATE_LIMITING_H_ diff --git a/keystore/tests/Android.bp b/keystore/tests/Android.bp index 227f88f4..c3f51775 100644 --- a/keystore/tests/Android.bp +++ b/keystore/tests/Android.bp @@ -10,11 +10,13 @@ cc_test { srcs: [ "auth_token_table_test.cpp", "auth_token_formatting_test.cpp", + "confirmationui_rate_limiting_test.cpp", "gtest_main.cpp", ], name: "keystore_unit_tests", tags: ["test"], static_libs: [ + "android.hardware.confirmationui@1.0", "libbase", "libgtest_main", "libhidlbase", diff --git a/keystore/tests/confirmationui_rate_limiting_test.cpp b/keystore/tests/confirmationui_rate_limiting_test.cpp new file mode 100644 index 00000000..f56b5099 --- /dev/null +++ b/keystore/tests/confirmationui_rate_limiting_test.cpp @@ -0,0 +1,274 @@ +/* + * Copyright (C) 2018 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <gtest/gtest.h> + +#include "../confirmationui_rate_limiting.h" +#include <keymaster/logger.h> + +using std::vector; + +namespace keystore { + +namespace test { + +namespace { + +class StdoutLogger : public ::keymaster::Logger { + public: + StdoutLogger() { set_instance(this); } + + int log_msg(LogLevel level, const char* fmt, va_list args) const { + int output_len = 0; + switch (level) { + case DEBUG_LVL: + output_len = printf("DEBUG: "); + break; + case INFO_LVL: + output_len = printf("INFO: "); + break; + case WARNING_LVL: + output_len = printf("WARNING: "); + break; + case ERROR_LVL: + output_len = printf("ERROR: "); + break; + case SEVERE_LVL: + output_len = printf("SEVERE: "); + break; + } + + output_len += vprintf(fmt, args); + output_len += printf("\n"); + return output_len; + } +}; + +StdoutLogger logger; + +class FakeClock : public std::chrono::steady_clock { + private: + static time_point sNow; + + public: + static void setNow(time_point newNow) { sNow = newNow; } + static time_point now() noexcept { return sNow; } +}; + +FakeClock::time_point FakeClock::sNow; + +} // namespace + +/* + * Test that there are no residual slots when various apps receive successful confirmations. + */ +TEST(ConfirmationUIRateLimitingTest, noPenaltyTest) { + auto now = std::chrono::steady_clock::now(); + RateLimiting<FakeClock> rateLimiting; + FakeClock::setNow(now); + + for (int i = 0; i < 10000; ++i) { + ASSERT_TRUE(rateLimiting.tryPrompt(rand())); + rateLimiting.processResult(ConfirmationResponseCode::OK); + } + + ASSERT_EQ(0U, rateLimiting.usedSlots()); +} + +TEST(ConfirmationUIRateLimitingTest, policyTest) { + using namespace std::chrono_literals; + auto now = std::chrono::steady_clock::now(); + RateLimiting<FakeClock> rateLimiting; + FakeClock::setNow(now); + + // first three tries are free + for (int i = 0; i < 3; ++i) { + ASSERT_TRUE(rateLimiting.tryPrompt(20)); + rateLimiting.processResult(ConfirmationResponseCode::Canceled); + } + + // throw in a couple of successful confirmations by other apps to make sure there + // is not cross talk + for (int i = 0; i < 10000; ++i) { + uid_t id = rand(); + if (id == 20) continue; + ASSERT_TRUE(rateLimiting.tryPrompt(id)); + rateLimiting.processResult(ConfirmationResponseCode::OK); + } + + // the next three tries get a 30s penalty + for (int i = 3; i < 6; ++i) { + FakeClock::setNow(FakeClock::now() + 29s); + ASSERT_FALSE(rateLimiting.tryPrompt(20)); + FakeClock::setNow(FakeClock::now() + 1s); + ASSERT_TRUE(rateLimiting.tryPrompt(20)); + rateLimiting.processResult(ConfirmationResponseCode::Canceled); + } + + // throw in a couple of successful confirmations by other apps to make sure there + // is not cross talk + for (int i = 0; i < 10000; ++i) { + uid_t id = rand(); + if (id == 20) continue; + ASSERT_TRUE(rateLimiting.tryPrompt(id)); + rateLimiting.processResult(ConfirmationResponseCode::OK); + } + + // there after the penalty doubles with each cancellation + for (int i = 6; i < 17; ++i) { + FakeClock::setNow((FakeClock::now() + 60s * (1ULL << (i - 6))) - 1s); + ASSERT_FALSE(rateLimiting.tryPrompt(20)); + FakeClock::setNow(FakeClock::now() + 1s); + ASSERT_TRUE(rateLimiting.tryPrompt(20)); + rateLimiting.processResult(ConfirmationResponseCode::Canceled); + } + + // throw in a couple of successful confirmations by other apps to make sure there + // is not cross talk + for (int i = 0; i < 10000; ++i) { + uid_t id = rand(); + if (id == 20) continue; + ASSERT_TRUE(rateLimiting.tryPrompt(id)); + rateLimiting.processResult(ConfirmationResponseCode::OK); + } + + ASSERT_EQ(1U, rateLimiting.usedSlots()); + + FakeClock::setNow(FakeClock::now() + 24h - 1s); + ASSERT_FALSE(rateLimiting.tryPrompt(20)); + + // after 24h the counter is forgotten + FakeClock::setNow(FakeClock::now() + 1s); + ASSERT_TRUE(rateLimiting.tryPrompt(20)); + rateLimiting.processResult(ConfirmationResponseCode::Canceled); + + // throw in a couple of successful confirmations by other apps to make sure there + // is not cross talk + for (int i = 0; i < 10000; ++i) { + uid_t id = rand(); + if (id == 20) continue; + ASSERT_TRUE(rateLimiting.tryPrompt(id)); + rateLimiting.processResult(ConfirmationResponseCode::OK); + } + + for (int i = 1; i < 3; ++i) { + ASSERT_TRUE(rateLimiting.tryPrompt(20)); + rateLimiting.processResult(ConfirmationResponseCode::Canceled); + } + + // throw in a couple of successful confirmations by other apps to make sure there + // is not cross talk + for (int i = 0; i < 10000; ++i) { + uid_t id = rand(); + if (id == 20) continue; + ASSERT_TRUE(rateLimiting.tryPrompt(id)); + rateLimiting.processResult(ConfirmationResponseCode::OK); + } + + for (int i = 3; i < 6; ++i) { + FakeClock::setNow(FakeClock::now() + 29s); + ASSERT_FALSE(rateLimiting.tryPrompt(20)); + FakeClock::setNow(FakeClock::now() + 1s); + ASSERT_TRUE(rateLimiting.tryPrompt(20)); + rateLimiting.processResult(ConfirmationResponseCode::Canceled); + } + + // throw in a couple of successful confirmations by other apps to make sure there + // is not cross talk + for (int i = 0; i < 10000; ++i) { + uid_t id = rand(); + if (id == 20) continue; + ASSERT_TRUE(rateLimiting.tryPrompt(id)); + rateLimiting.processResult(ConfirmationResponseCode::OK); + } + + for (int i = 6; i < 17; ++i) { + FakeClock::setNow((FakeClock::now() + 60s * (1ULL << (i - 6))) - 1s); + ASSERT_FALSE(rateLimiting.tryPrompt(20)); + FakeClock::setNow(FakeClock::now() + 1s); + ASSERT_TRUE(rateLimiting.tryPrompt(20)); + rateLimiting.processResult(ConfirmationResponseCode::Canceled); + } + + // throw in a couple of successful confirmations by other apps to make sure there + // is not cross talk + for (int i = 0; i < 10000; ++i) { + uid_t id = rand(); + if (id == 20) continue; + ASSERT_TRUE(rateLimiting.tryPrompt(id)); + rateLimiting.processResult(ConfirmationResponseCode::OK); + } + + ASSERT_EQ(1U, rateLimiting.usedSlots()); +} + +TEST(ConfirmationUIRateLimitingTest, rewindTest) { + using namespace std::chrono_literals; + auto now = std::chrono::steady_clock::now(); + RateLimiting<FakeClock> rateLimiting; + + // first three tries are free + for (int i = 0; i < 3; ++i) { + FakeClock::setNow(now); + ASSERT_TRUE(rateLimiting.tryPrompt(20)); + rateLimiting.processResult(ConfirmationResponseCode::Canceled); + } + + for (int i = 3; i < 6; ++i) { + FakeClock::setNow(FakeClock::now() + 29s); + ASSERT_FALSE(rateLimiting.tryPrompt(20)); + FakeClock::setNow(FakeClock::now() + 1s); + ASSERT_TRUE(rateLimiting.tryPrompt(20)); + rateLimiting.processResult(ConfirmationResponseCode::Canceled); + } + + FakeClock::setNow(FakeClock::now() + 59s); + ASSERT_FALSE(rateLimiting.tryPrompt(20)); + FakeClock::setNow(FakeClock::now() + 1s); + ASSERT_TRUE(rateLimiting.tryPrompt(20)); + rateLimiting.processResult(ConfirmationResponseCode::Aborted); + + FakeClock::setNow(FakeClock::now() - 1s); + ASSERT_FALSE(rateLimiting.tryPrompt(20)); + FakeClock::setNow(FakeClock::now() + 1s); + ASSERT_TRUE(rateLimiting.tryPrompt(20)); + rateLimiting.processResult(ConfirmationResponseCode::SystemError); + + // throw in a couple of successful confirmations by other apps to make sure there + // is not cross talk + for (int i = 0; i < 10000; ++i) { + uid_t id = rand(); + if (id == 20) continue; + ASSERT_TRUE(rateLimiting.tryPrompt(id)); + rateLimiting.processResult(ConfirmationResponseCode::OK); + } + + FakeClock::setNow(FakeClock::now() - 1s); + ASSERT_FALSE(rateLimiting.tryPrompt(20)); + FakeClock::setNow(FakeClock::now() + 1s); + ASSERT_TRUE(rateLimiting.tryPrompt(20)); + rateLimiting.processResult(ConfirmationResponseCode::UIError); + + FakeClock::setNow(FakeClock::now() - 1s); + ASSERT_FALSE(rateLimiting.tryPrompt(20)); + FakeClock::setNow(FakeClock::now() + 1s); + ASSERT_TRUE(rateLimiting.tryPrompt(20)); + + ASSERT_EQ(1U, rateLimiting.usedSlots()); +} + +} // namespace test +} // namespace keystore |