Validate token using PBKDF2-HMAC-SHA1.

This change should make token validation more resilient to:
 - timing attacks (constant time array compare)
 - brute-force/dictionary attacks (PBKDF2)

 Closes #220
pull/230/merge
Nils Maier 2014-04-19 19:10:06 +02:00
parent 98ba096951
commit 82dad90ff3
4 changed files with 110 additions and 6 deletions

View File

@ -73,6 +73,15 @@
#ifdef ENABLE_WEBSOCKET
# include "WebSocketSessionMan.h"
#endif // ENABLE_WEBSOCKET
#include "Option.h"
#include "util_security.h"
// Lower time limit for PBKDF2 operations in validateToken.
static const double kTokenTimeLower = 0.055;
// Upper time limit for PBKDF2 operations in validateToken.
static const double kTokenTimeUpper = 0.120;
// Sweet spot time for PBKDF2 operations in validateToken.
static const double kTokenTimeSweetspot = 0.072;
namespace aria2 {
@ -102,7 +111,8 @@ DownloadEngine::DownloadEngine(std::unique_ptr<EventPoll> eventPoll)
asyncDNSServers_(nullptr),
#endif // HAVE_ARES_ADDR_NODE
dnsCache_(make_unique<DNSCache>()),
option_(nullptr)
option_(nullptr),
tokenIterations_(5000)
{
unsigned char sessionId[20];
util::generateRandomKey(sessionId);
@ -631,4 +641,84 @@ void DownloadEngine::setWebSocketSessionMan
}
#endif // ENABLE_WEBSOCKET
bool DownloadEngine::validateToken(const std::string& token)
{
using namespace util::security;
if (!option_->defined(PREF_RPC_SECRET)) {
return true;
}
if (!tokenHMAC_ || tokenAverageDuration_ > kTokenTimeUpper ||
tokenAverageDuration_ < kTokenTimeLower) {
// Setup our stuff.
if (tokenHMAC_) {
A2_LOG_INFO(fmt("Recalculating iterations because avg. duration is %.4f",
tokenAverageDuration_));
}
tokenHMAC_ = HMAC::createRandom();
if (!tokenHMAC_) {
A2_LOG_ERROR("Failed to create HMAC");
return false;
}
// This should still be pretty fast on a modern system... Well, too fast
// with the initial 5000 iterations, and that is why we adjust it.
// XXX We should run this setup high priorty, so that other processes on the
// system don't mess up our results and let us underestimate the iterations.
std::deque<double> mm;
for (auto i = 0; i < 10; ++i) {
auto c = std::clock();
tokenExpected_ = make_unique<HMACResult>(
PBKDF2(tokenHMAC_.get(), option_->get(PREF_RPC_SECRET),
tokenIterations_));
mm.push_back((std::clock() - c) / (double)CLOCKS_PER_SEC);
}
std::sort(mm.begin(), mm.end());
// Pop outliers.
mm.pop_front();
mm.pop_back();
mm.pop_back();
auto duration = std::accumulate(mm.begin(), mm.end(), 0.0) / mm.size();
A2_LOG_INFO(fmt("Took us %.4f secs on average to perform PBKDF2 with %zu "
"iterations during setup",
duration, tokenIterations_));
// Adjust iterations so that an op takes about |kTokenTimeSpeetspot| sec,
// which would allow for a couple attempts per second (instead of
// potentially thousands without PBKDF2).
// We might overestimate the performance a bit, but should not perform
// worse than |kTokenTimeUpper| secs per attempt on a normally loaded system
// and no better than |kTokenTimeLower|. If this does not hold true anymore,
// the |tokenAverageDuration_| checks will force a re-calcuation.
tokenIterations_ *= kTokenTimeSweetspot / duration;
auto c = std::clock();
tokenExpected_ = make_unique<HMACResult>(
PBKDF2(tokenHMAC_.get(), option_->get(PREF_RPC_SECRET),
tokenIterations_));
duration = (std::clock() - c) / (double)CLOCKS_PER_SEC;
A2_LOG_INFO(fmt("Took us %.4f secs to perform PBKDF2 with %zu iterations",
duration, tokenIterations_));
// Seed average duration.
tokenAverageDuration_ = duration;
}
auto c = std::clock();
bool rv = *tokenExpected_ == PBKDF2(tokenHMAC_.get(), token,
tokenIterations_);
auto duration = (std::clock() - c) / (double)CLOCKS_PER_SEC;
A2_LOG_DEBUG(fmt("Took us %.4f secs to perform token compare with %zu "
"iterations",
duration, tokenIterations_));
// Update rolling hash.
tokenAverageDuration_ = tokenAverageDuration_ * 0.9 + duration * 0.1;
return rv;
}
} // namespace aria2

View File

@ -74,6 +74,13 @@ class WebSocketSessionMan;
} // namespace rpc
#endif // ENABLE_WEBSOCKET
namespace util {
namespace security {
class HMAC;
class HMACResult;
} // namespace security
} // namespace util
class DownloadEngine {
private:
void waitData();
@ -173,6 +180,13 @@ private:
// deleted.
std::deque<std::unique_ptr<Command>> routineCommands_;
std::deque<std::unique_ptr<Command>> commands_;
std::unique_ptr<util::security::HMAC> tokenHMAC_;
std::unique_ptr<util::security::HMACResult> tokenExpected_;
size_t tokenIterations_;
double tokenAverageDuration_;
public:
DownloadEngine(std::unique_ptr<EventPoll> eventPoll);
@ -360,6 +374,8 @@ public:
return webSocketSessionMan_;
}
#endif // ENABLE_WEBSOCKET
bool validateToken(const std::string& token);
};
} // namespace aria2

View File

@ -85,10 +85,8 @@ void RpcMethod::authorize(RpcRequest& req, DownloadEngine* e)
}
}
}
if(e && e->getOption()->defined(PREF_RPC_SECRET)) {
if(token != e->getOption()->get(PREF_RPC_SECRET)) {
throw DL_ABORT_EX("Unauthorized");
}
if (!e || !e->validateToken(token)) {
throw DL_ABORT_EX("Unauthorized");
}
}

View File

@ -723,7 +723,7 @@ void RpcMethodTest::testChangeGlobalOption_withNotAllowedOption()
void RpcMethodTest::testNoSuchMethod()
{
NoSuchMethodRpcMethod m;
auto res = m.execute(createReq("make.hamburger"), nullptr);
auto res = m.execute(createReq("make.hamburger"), e_.get());
CPPUNIT_ASSERT_EQUAL(1, res.code);
CPPUNIT_ASSERT_EQUAL(std::string("No such method: make.hamburger"),
getString(downcast<Dict>(res.param), "faultString"));