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:
parent
eec88d343a
commit
064ce85612
5 changed files with 411 additions and 0 deletions
|
@ -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);
|
||||
|
|
|
@ -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
|
||||
|
|
124
keystore/confirmationui_rate_limiting.h
Normal file
124
keystore/confirmationui_rate_limiting.h
Normal 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_
|
|
@ -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",
|
||||
|
|
274
keystore/tests/confirmationui_rate_limiting_test.cpp
Normal file
274
keystore/tests/confirmationui_rate_limiting_test.cpp
Normal 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
|
Loading…
Reference in a new issue