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/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