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
This commit is contained in:
Janis Danisevskis 2018-03-12 16:49:16 -07:00
parent eec88d343a
commit 064ce85612
5 changed files with 411 additions and 0 deletions

View file

@ -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);

View file

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

View file

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

View file

@ -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",

View file

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