/* ** ** 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 #include #include #include #include #include namespace keystore { using ConfirmationResponseCode = android::hardware::confirmationui::V1_0::ResponseCode; using std::chrono::duration; using std::chrono::time_point; template 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 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; } // The app is penalized for cancelling a request. Request are rolled back only if // the prompt was cancelled by the system: e.g. a system error or asynchronous event. // When the user cancels the prompt, it is subject to rate limiting. static constexpr const uint_t kInvalidRequester = -1; void cancelPrompt() { latest_requester_ = kInvalidRequester; } void processResult(ConfirmationResponseCode rc) { if (latest_requester_ == kInvalidRequester) { return; } 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_