mirror of https://github.com/aria2/aria2
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 #220pull/230/merge
parent
98ba096951
commit
82dad90ff3
|
@ -73,6 +73,15 @@
|
||||||
#ifdef ENABLE_WEBSOCKET
|
#ifdef ENABLE_WEBSOCKET
|
||||||
# include "WebSocketSessionMan.h"
|
# include "WebSocketSessionMan.h"
|
||||||
#endif // ENABLE_WEBSOCKET
|
#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 {
|
namespace aria2 {
|
||||||
|
|
||||||
|
@ -102,7 +111,8 @@ DownloadEngine::DownloadEngine(std::unique_ptr<EventPoll> eventPoll)
|
||||||
asyncDNSServers_(nullptr),
|
asyncDNSServers_(nullptr),
|
||||||
#endif // HAVE_ARES_ADDR_NODE
|
#endif // HAVE_ARES_ADDR_NODE
|
||||||
dnsCache_(make_unique<DNSCache>()),
|
dnsCache_(make_unique<DNSCache>()),
|
||||||
option_(nullptr)
|
option_(nullptr),
|
||||||
|
tokenIterations_(5000)
|
||||||
{
|
{
|
||||||
unsigned char sessionId[20];
|
unsigned char sessionId[20];
|
||||||
util::generateRandomKey(sessionId);
|
util::generateRandomKey(sessionId);
|
||||||
|
@ -631,4 +641,84 @@ void DownloadEngine::setWebSocketSessionMan
|
||||||
}
|
}
|
||||||
#endif // ENABLE_WEBSOCKET
|
#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
|
} // namespace aria2
|
||||||
|
|
|
@ -74,6 +74,13 @@ class WebSocketSessionMan;
|
||||||
} // namespace rpc
|
} // namespace rpc
|
||||||
#endif // ENABLE_WEBSOCKET
|
#endif // ENABLE_WEBSOCKET
|
||||||
|
|
||||||
|
namespace util {
|
||||||
|
namespace security {
|
||||||
|
class HMAC;
|
||||||
|
class HMACResult;
|
||||||
|
} // namespace security
|
||||||
|
} // namespace util
|
||||||
|
|
||||||
class DownloadEngine {
|
class DownloadEngine {
|
||||||
private:
|
private:
|
||||||
void waitData();
|
void waitData();
|
||||||
|
@ -173,6 +180,13 @@ private:
|
||||||
// deleted.
|
// deleted.
|
||||||
std::deque<std::unique_ptr<Command>> routineCommands_;
|
std::deque<std::unique_ptr<Command>> routineCommands_;
|
||||||
std::deque<std::unique_ptr<Command>> commands_;
|
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:
|
public:
|
||||||
DownloadEngine(std::unique_ptr<EventPoll> eventPoll);
|
DownloadEngine(std::unique_ptr<EventPoll> eventPoll);
|
||||||
|
|
||||||
|
@ -360,6 +374,8 @@ public:
|
||||||
return webSocketSessionMan_;
|
return webSocketSessionMan_;
|
||||||
}
|
}
|
||||||
#endif // ENABLE_WEBSOCKET
|
#endif // ENABLE_WEBSOCKET
|
||||||
|
|
||||||
|
bool validateToken(const std::string& token);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace aria2
|
} // namespace aria2
|
||||||
|
|
|
@ -85,10 +85,8 @@ void RpcMethod::authorize(RpcRequest& req, DownloadEngine* e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if(e && e->getOption()->defined(PREF_RPC_SECRET)) {
|
if (!e || !e->validateToken(token)) {
|
||||||
if(token != e->getOption()->get(PREF_RPC_SECRET)) {
|
throw DL_ABORT_EX("Unauthorized");
|
||||||
throw DL_ABORT_EX("Unauthorized");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -723,7 +723,7 @@ void RpcMethodTest::testChangeGlobalOption_withNotAllowedOption()
|
||||||
void RpcMethodTest::testNoSuchMethod()
|
void RpcMethodTest::testNoSuchMethod()
|
||||||
{
|
{
|
||||||
NoSuchMethodRpcMethod m;
|
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(1, res.code);
|
||||||
CPPUNIT_ASSERT_EQUAL(std::string("No such method: make.hamburger"),
|
CPPUNIT_ASSERT_EQUAL(std::string("No such method: make.hamburger"),
|
||||||
getString(downcast<Dict>(res.param), "faultString"));
|
getString(downcast<Dict>(res.param), "faultString"));
|
||||||
|
|
Loading…
Reference in New Issue