Skip to content
Open
Show file tree
Hide file tree
Changes from 8 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
05fddaf
Enhance OtpSystem with improved code handling
Koaio Mar 10, 2026
2e42714
Enhance OtpSystem with configurable settings
Koaio Mar 10, 2026
00d8d4a
Update PrivateChatOtpConstant value
Koaio Mar 10, 2026
7424cb9
Merge branch 'master' into Harden-OTP-system
Koaio Mar 10, 2026
b35da4b
Add default constructor to OtpSystem Settings
Koaio Mar 10, 2026
404492f
Add default constructor for OtpSystem
Koaio Mar 10, 2026
f401dfd
Refactor OtpSystem constructor overloads
Koaio Mar 10, 2026
c4e008a
Change code generation to use uint32_t type
Koaio Mar 10, 2026
b26f90c
Update include/server/system/OtpSystem.hpp
Koaio Mar 11, 2026
de2fe62
Update include/server/system/OtpSystem.hpp
Koaio Mar 11, 2026
a133f76
Update include/server/system/OtpSystem.hpp
Koaio Mar 11, 2026
293db6f
Update include/server/system/OtpSystem.hpp
Koaio Mar 11, 2026
38f5b15
Merge branch 'Story-Of-Alicia:master' into Harden-OTP-system
Koaio Mar 12, 2026
f36079e
Merge branch 'master' into Harden-OTP-system
Koaio Mar 13, 2026
d954d14
Update OtpSystem.hpp
Koaio Mar 14, 2026
d5b80a1
Merge branch 'master' into Harden-OTP-system
Koaio Mar 14, 2026
c87ae10
Merge branch 'Story-Of-Alicia:master' into Harden-OTP-system
Koaio Mar 15, 2026
143775d
config: add Otp settings struct to Config
Koaio Mar 15, 2026
b48a7fd
config: parse OTP section from server config file
Koaio Mar 15, 2026
e373823
OtpSystem: take ServerInstance& and drop local Settings
Koaio Mar 15, 2026
0c966b2
OtpSystem: read config from ServerInstance::GetSettings().otp
Koaio Mar 15, 2026
7719f81
ServerInstance: construct OtpSystem with *this
Koaio Mar 15, 2026
dc1e4c9
OtpSystem: merge AttemptRecord into Code, drop _attempts map
Koaio Mar 15, 2026
33ec0f6
OtpSystem: invalidate code after 5 failed attempts, single _codes map
Koaio Mar 15, 2026
6dde14c
config: remove lockoutDuration from Otp settings
Koaio Mar 15, 2026
aa545b8
config: stop parsing otp lockout_duration
Koaio Mar 15, 2026
3a2a90e
Merge branch 'master' into Harden-OTP-system
Koaio Mar 15, 2026
316fa01
Merge branch 'master' into Harden-OTP-system
Koaio Mar 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion include/server/chat/PrivateChatDirector.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ namespace server
{

//! Chat client OTP constant
constexpr uint32_t PrivateChatOtpConstant = 0x14E05CE5;
constexpr uint32_t PrivateChatOtpConstant = 0x7C3D91A2;

class ServerInstance;

Expand Down
45 changes: 42 additions & 3 deletions include/server/system/OtpSystem.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@
#define OTPSYSTEM_HPP

#include <chrono>
#include <random>
#include <mutex>
#include <random>
#include <unordered_map>

namespace server
Expand All @@ -16,21 +16,60 @@ namespace server
class OtpSystem
{
public:
struct Settings

@rgnter rgnter Mar 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This setting struct should be defined in Settings.hpp header. See AuthenticationService for example..

{
std::chrono::seconds codeTtl{30};
uint32_t maxFailedAttempts{5};
std::chrono::seconds lockoutDuration{60};
std::chrono::seconds purgeInterval{60};

Settings() = default;
};

OtpSystem();
explicit OtpSystem(Settings settings);

@rgnter rgnter Mar 11, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should take forward-definition of ServerInstance and get config by accessing GetSettings and taking a reference (so reloading of configs can be implemented easily). For example see AuthenticationService.


//! Grants a one-time code for the given key.
//! If a code already exists for this key, it is replaced.
//! @param key Identity key the code is bound to.
//! @return The generated one-time code.
uint32_t GrantCode(size_t key);
Comment thread
Koaio marked this conversation as resolved.
Outdated

//! Authorizes a one-time code for the given key.
//! Enforces brute-force protection: after too many failed attempts,
//! the key is temporarily locked out.
Comment thread
Koaio marked this conversation as resolved.
Outdated
//! @param key Identity key the code is bound to.
//! @param code The code to verify.
//! @param consume If true, the code is consumed (erased) on success.
//! @return True if the code is valid and the key is not locked out.
Comment thread
Koaio marked this conversation as resolved.
Outdated
bool AuthorizeCode(size_t key, uint32_t code, bool consume = true);
Comment thread
Koaio marked this conversation as resolved.
Outdated

//! Removes all expired codes and stale lockout entries.
//! Called automatically on a periodic basis, but can be invoked manually.
void PurgeExpired();

private:
Settings _settings;

struct Code
{
std::chrono::steady_clock::time_point expiry{};
uint32_t code{};
};

struct AttemptRecord
{
uint32_t failedAttempts{0};
std::chrono::steady_clock::time_point lockoutExpiry{};
};

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please merge AttemptRecord with the Code struct.

  struct Code
  {
    //! A time point of when the code expires.
    std::chrono::steady_clock::time_point expiry{};
    //! A counter of authorization attempts.
    size_t authorizationAttempts{};
    //! A value of the code.
    uint32_t code{};
  };

Also if you read previous comments, reference to the ServerInstance should be taken and access to settings should be done by reference.

std::mutex _codesMutex;
std::random_device _rd;
std::mt19937 _rng;
std::unordered_map<size_t, Code> _codes;
std::unordered_map<size_t, AttemptRecord> _attempts;
std::chrono::steady_clock::time_point _lastPurge;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not necessary. Invalidate the key/code after 5 unsuccessful attempts to use it.

};

} // namespace server

#endif //OTPSYSTEM_HPP
#endif // OTPSYSTEM_HPP
113 changes: 106 additions & 7 deletions src/server/system/OtpSystem.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -4,37 +4,136 @@

#include "server/system/OtpSystem.hpp"

#include <spdlog/spdlog.h>

namespace server
{

OtpSystem::OtpSystem()
: OtpSystem(Settings{})
{
}

OtpSystem::OtpSystem(Settings settings)
: _settings(std::move(settings))
, _rng(std::random_device{}())
, _lastPurge(std::chrono::steady_clock::now())
{
}

uint32_t OtpSystem::GrantCode(const size_t key)
{
std::scoped_lock lock(_codesMutex);
const auto [iter, inserted] = _codes.insert_or_assign(

PurgeExpired();

_codes.insert_or_assign(
key,
Code{
.expiry = std::chrono::steady_clock::now() + std::chrono::seconds(30),
.code = _rd()});
.expiry = std::chrono::steady_clock::now() + _settings.codeTtl,
.code = static_cast<uint32_t>(_rng())});

return iter->second.code;
// Reset failed attempts when a new code is granted for this key,
// since the legitimate server is issuing a fresh code.
_attempts.erase(key);

return _codes.at(key).code;
}

bool OtpSystem::AuthorizeCode(const size_t key, const uint32_t code, bool consume)
{
std::scoped_lock lock(_codesMutex);

const auto now = std::chrono::steady_clock::now();

// Check brute-force lockout.
if (const auto attemptIter = _attempts.find(key); attemptIter != _attempts.end())
{
const auto& record = attemptIter->second;
if (record.failedAttempts >= _settings.maxFailedAttempts
&& now < record.lockoutExpiry)
{
spdlog::warn("OTP authorization for key {} denied: locked out after {} failed attempts",
key, record.failedAttempts);
return false;
}
}

const auto codeIter = _codes.find(key);
if (codeIter == _codes.cend())
{
auto& record = _attempts[key];
record.failedAttempts++;
if (record.failedAttempts >= _settings.maxFailedAttempts)
record.lockoutExpiry = now + _settings.lockoutDuration;
return false;
}

const Code& ctx = codeIter->second;

const bool expired = std::chrono::steady_clock::now() > ctx.expiry;
const bool expired = now > ctx.expiry;
const bool authorized = not expired && ctx.code == code;
if (authorized && consume)
_codes.erase(codeIter);

if (authorized)
{
if (consume)
_codes.erase(codeIter);

_attempts.erase(key);
}
else
{
auto& record = _attempts[key];
record.failedAttempts++;

if (record.failedAttempts >= _settings.maxFailedAttempts)
{
record.lockoutExpiry = now + _settings.lockoutDuration;
spdlog::warn("OTP key {} locked out for {}s after {} failed attempts",
key,
_settings.lockoutDuration.count(),
record.failedAttempts);
}

if (expired)
_codes.erase(codeIter);
}

return authorized;
}

void OtpSystem::PurgeExpired()
{
const auto now = std::chrono::steady_clock::now();

if (now - _lastPurge < _settings.purgeInterval)
return;

_lastPurge = now;

// Purge expired codes.
for (auto it = _codes.begin(); it != _codes.end();)
{
if (now > it->second.expiry)
it = _codes.erase(it);
else
++it;
}

// Purge stale lockout records whose lockout has expired
// and whose failure count is below the threshold (i.e. old records).
for (auto it = _attempts.begin(); it != _attempts.end();)
{
const auto& record = it->second;
if (record.failedAttempts >= _settings.maxFailedAttempts
&& now > record.lockoutExpiry)
it = _attempts.erase(it);
else if (record.failedAttempts < _settings.maxFailedAttempts
&& now > record.lockoutExpiry)
it = _attempts.erase(it);
else
++it;
}
}

} // namespace server
Loading