summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorJanis Danisevskis <jdanis@google.com>2018-03-12 16:49:16 -0700
committerJanis Danisevskis <jdanis@google.com>2018-03-14 13:36:56 -0700
commit064ce8561211612a4bf08dd2134657ad855bc801 (patch)
tree7bd5f6700485917132ad860a1a0e3bdb1dea591f
parenteec88d343a1105d4da5c108c49258e2ccc131f4a (diff)
downloadsecurity-064ce8561211612a4bf08dd2134657ad855bc801.tar.gz
Implement rate limiting on a per app basis for confirmationui
Implements the following strategy per app: * Every attempted prompt increments a try counter. * A prompt that was confirmed by the user resets the try counter. * No penalty is applied after the first two cancelled attempts. * A penalty of 30s is applied after attempt 3, 4, and 5 when cancelled by the user * A penalty of 60s * 2**(N - 6) after the Nth cancelled attempt for attempts 6... * A try counter that was not updated in 24h gets garbage collected. Test: /data/nativetest64/keystore_unit_tests/keystore_unit_tests Bug: 73892492 Change-Id: I0b50869259bfe920338c0c049cb9a715143ab103
-rw-r--r--keystore/confirmation_manager.cpp8
-rw-r--r--keystore/confirmation_manager.h3
-rw-r--r--keystore/confirmationui_rate_limiting.h124
-rw-r--r--keystore/tests/Android.bp2
-rw-r--r--keystore/tests/confirmationui_rate_limiting_test.cpp274
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