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