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
diff --git a/keystore/confirmation_manager.cpp b/keystore/confirmation_manager.cpp
index acca304..0dee4aa 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 @@
         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 @@
     // 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 b92deda..46b623c 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 @@
     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 0000000..12c20fa
--- /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 227f88f..c3f5177 100644
--- a/keystore/tests/Android.bp
+++ b/keystore/tests/Android.bp
@@ -10,11 +10,13 @@
     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 0000000..f56b509
--- /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