10static const char*
TAG =
"2FA";
37 LOG_W(
TAG,
"Invalid OATH type %u", type);
46 LOG_W(
TAG,
"CR algorithm %u unsupported (expected SHA1 or SHA256)", algorithm);
55 LOG_W(
TAG,
"Invalid OATH digits %u, expected %u-%u",
65 LOG_W(
TAG,
"Invalid TOTP period %lu, expected %u-%u",
72 LOG_W(
TAG,
"Invalid OATH algorithm %u", algorithm);
99 if (c >=
'A' && c <=
'Z')
return c -
'A';
100 if (c >=
'a' && c <=
'z')
return c -
'a';
101 if (c >=
'2' && c <=
'7')
return c -
'2' + 26;
112static int base32Decode(
const char* encoded, uint8_t* out,
size_t outMax) {
113 if (!encoded || !out)
return -1;
119 for (
const char* p = encoded; *p; ++p) {
120 if (*p ==
'=' || *p ==
' ' || *p ==
'\t' || *p ==
'\r' || *p ==
'\n') {
127 buffer = (buffer << 5) | value;
131 if (count >= outMax) {
134 out[count++] =
static_cast<uint8_t
>((buffer >> bitsLeft) & 0xFF);
138 return static_cast<int>(count);
142 1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000
150 static OathStore inst;
169 if (!out)
return false;
170 if (!slots_.hasSlotRange())
return false;
171 uint16_t physSlot = 0;
175 if (!se)
return false;
177 cdc::hal::ISecureElement::RMemHeader header = {};
179 uint16_t payloadLen = 0;
181 auto res = se->rmemReadWithHeader(physSlot, &header, payloadBuf,
sizeof(payloadBuf), &payloadLen);
186 if (header.moduleId != slots_.moduleId()) {
193 LOG_W(
TAG,
"Slot %u payload size mismatch (%u != %u), rejecting",
194 physSlot, payloadLen,
static_cast<unsigned>(
sizeof(
OathPayload)));
199 memcpy(&payload, payloadBuf,
sizeof(payload));
201 memset(out, 0,
sizeof(*out));
205 strncpy(out->
name, header.name,
sizeof(out->
name) - 1);
211 ?
static_cast<uint8_t
>(
sizeof(out->
secret))
238 const char* issuer,
const uint8_t* secret, uint8_t secretLen,
239 uint8_t digits, uint32_t period, uint64_t counter,
240 uint8_t algorithm, uint8_t
flags) {
244 strncpy(payload.
issuer, issuer,
sizeof(payload.
issuer) - 1);
246 memcpy(payload.
secret, secret, secretLen);
255 if (!se)
return false;
258 auto res = se->rmemWriteWithHeader(
263 reinterpret_cast<const uint8_t*
>(&payload),
268 LOG_E(
TAG,
"Failed to write slot %u", physSlot);
290 const char* secretBase32, uint8_t digits, uint32_t period,
291 uint8_t algorithm, uint64_t counter, uint8_t
flags) {
292 if (!
name || !secretBase32)
return false;
293 if (!slots_.hasSlotRange())
return false;
296 LOG_W(
TAG,
"OATH name too long (max %u)",
306 if (!slots_.findFreeSlot(&slot)) {
313 if (secretLen <= 0) {
314 LOG_E(
TAG,
"Invalid Base32 secret");
319 static_cast<uint8_t
>(secretLen), digits, period,
320 counter, algorithm,
flags);
338 const char* secretBase32, uint8_t digits, uint32_t period,
339 uint8_t algorithm, uint64_t counter, uint8_t
flags) {
340 if (!
name || !secretBase32)
return false;
341 if (!slots_.hasSlotRange())
return false;
343 LOG_W(
TAG,
"OATH name too long (max %u)",
350 uint16_t physSlot = 0;
355 if (secretLen <= 0) {
356 LOG_E(
TAG,
"Invalid Base32 secret");
361 static_cast<uint8_t
>(secretLen), digits, period,
362 counter, algorithm,
flags);
371 if (!slots_.hasSlotRange())
return false;
372 uint16_t physSlot = 0;
375 if (!se)
return false;
377 auto res = se->rmemErase(physSlot);
401uint32_t OathStore::generate(
const uint8_t* secret,
size_t secretLen, uint64_t counter,
403 if (!secret || secretLen == 0 || secretLen >
SECRET_LEN) {
411 uint8_t counterBytes[8];
412 uint64_t c = counter;
413 for (
int i = 7; i >= 0; i--) {
414 counterBytes[i] =
static_cast<uint8_t
>(c & 0xFF);
418 uint8_t hmac[64] = {};
420 if (!hmacCompute(algorithm, secret, secretLen, counterBytes, 8, hmac, &hmacLen)) {
424 int offset = hmac[hmacLen - 1] & 0x0F;
426 ((hmac[offset] & 0x7F) << 24) |
427 ((hmac[offset + 1] & 0xFF) << 16) |
428 ((hmac[offset + 2] & 0xFF) << 8) |
429 (hmac[offset + 3] & 0xFF);
445bool OathStore::hmacCompute(
OathAlgorithm algo,
const uint8_t* key,
size_t keyLen,
446 const uint8_t* data,
size_t dataLen,
447 uint8_t* output,
size_t* outputLen)
const {
448 mbedtls_md_type_t mdType;
453 mdType = MBEDTLS_MD_SHA256;
457 mdType = MBEDTLS_MD_SHA512;
461 mdType = MBEDTLS_MD_SHA1;
466 const mbedtls_md_info_t* mdInfo = mbedtls_md_info_from_type(mdType);
471 int ret = mbedtls_md_hmac(mdInfo, key, keyLen, data, dataLen, output);
477 *outputLen = expectedLen;
489bool OathStore::persistCounter(uint16_t slot,
const OathEntry& entry, uint64_t counter) {
490 uint16_t physSlot = 0;
493 entry.issuer[0] ? entry.issuer :
nullptr,
494 entry.secret, entry.secretLen, entry.digits, entry.period,
495 counter, entry.algorithm, entry.flags);
505static void formatCode(uint32_t code, uint8_t digits,
char* codeOut,
size_t codeOutLen) {
507 snprintf(fmt,
sizeof(fmt),
"%%0%ulu",
509 snprintf(codeOut, codeOutLen, fmt,
static_cast<unsigned long>(code));
521 if (!codeOut || codeOutLen == 0)
return -1;
530 uint64_t counter = account.
counter;
536 if (!persistCounter(slot, account, counter + 1)) {
537 LOG_E(
TAG,
"Failed to persist HOTP counter for slot %u", slot);
546 snprintf(codeOut, codeOutLen,
"------");
551 uint64_t counter =
static_cast<uint64_t
>(time(
nullptr)) / period;
566 return static_cast<uint8_t
>(period - (time(
nullptr) % period));
574 time_t now = time(
nullptr);
576 localtime_r(&now, &timeinfo);
577 return timeinfo.tm_year >= 124;
587 if (!
name || !slotOut)
return false;
588 if (!slots_.hasSlotRange())
return false;
594 } ctx = {
name, 0,
false };
597 auto* c =
static_cast<Ctx*
>(user);
598 if (c->found)
return;
600 uint16_t logical = 0;
614 if (!ctx.found)
return false;
635 uint8_t* out,
bool* touchRequiredOut) {
636 if (!entryName || !out)
return -1;
637 if (clen != 0 && !challenge)
return -1;
650 LOG_W(
TAG,
"Entry '%s' is not a CR entry", entryName);
659 LOG_W(
TAG,
"CR entry '%s' has unsupported algorithm %u", entryName, entry.
algorithm);
664 if (!hmacCompute(algo, entry.
secret, entry.
secretLen, challenge, clen, out, &outLen)) {
668 if (touchRequiredOut) {
671 return static_cast<int>(outLen);
680 if (!slotOut)
return false;
681 if (!slots_.hasSlotRange())
return false;
686 } ctx = { 0,
false };
689 auto* c =
static_cast<Ctx*
>(user);
690 if (c->found)
return;
691 uint16_t logical = 0;
708 if (!ctx.found)
return false;
722 uint8_t* out,
bool* touchRequiredOut) {
742 if (!slots_.hasSlotRange())
return;
746 } ctx = { keepSlot };
749 auto* c =
static_cast<Ctx*
>(user);
751 uint16_t logical = 0;
752 if (!store.toLogicalSlot(slot, &logical))
return;
753 if (logical == c->keep)
return;
755 if (!store.readAccount(logical, &entry))
return;
757 uint8_t cleared =
static_cast<uint8_t
>(entry.
flags &
~OathFlag::USB_CR_SLOT);
758 uint16_t physSlot = 0;
759 if (!store.toPhysicalSlot(logical, &physSlot))
return;
char name[cdc::hal::ISecureElement::RMEM_NAME_LEN]
CDC Log: logging over TinyUSB CDC and UART.
#define LOG_W(tag, fmt,...)
#define LOG_E(tag, fmt,...)
bool writeSlot(uint8_t moduleId, uint16_t slot, const char *name, uint8_t flags)
Writes or updates cached metadata entry for one slot.
bool eraseSlot(uint8_t moduleId, uint16_t slot)
Clears cached metadata entry for one slot.
static TropicStorage & instance()
Returns singleton instance of TROPIC metadata cache manager.
bool forEachSlot(uint8_t moduleId, SlotCallback cb, void *ctx)
Iterates all cached slots for one module across its allowed range.
static constexpr uint8_t RMEM_NAME_LEN
int challengeResponseUsbSlot(const uint8_t *challenge, size_t clen, uint8_t *out, bool *touchRequiredOut=nullptr)
Computes the raw HMAC challenge-response for the USB-CR slot entry.
bool toPhysicalSlot(uint16_t logicalIndex, uint16_t *slotOut) const
bool isTimeValid() const
Returns whether system time is considered valid for TOTP.
void clearUsbCrFlagExcept(uint16_t keepSlot)
Clears the USB-CR-slot flag on every entry except keepSlot.
int challengeResponse(const char *entryName, const uint8_t *challenge, size_t clen, uint8_t *out, bool *touchRequiredOut=nullptr)
Computes the raw HMAC challenge-response for a CR entry by name.
static constexpr uint8_t SECRET_LEN
bool toLogicalSlot(uint16_t slot, uint16_t *logicalIndexOut) const
bool findUsbCrSlot(uint16_t *slotOut) const
Finds the logical slot of the CR entry flagged as the USB-CR slot.
bool addAccount(uint8_t type, const char *name, const char *issuer, const char *secretBase32, uint8_t digits, uint32_t period, uint8_t algorithm, uint64_t counter, uint8_t flags=0)
Adds a new OATH entry from a Base32 secret.
static OathStore & instance()
Returns singleton OATH store instance.
static constexpr uint8_t DEFAULT_DIGITS
int8_t generateCode(uint16_t slot, char *codeOut, size_t codeOutLen)
Renders the current code for an entry into codeOut.
bool updateAccount(uint16_t slot, uint8_t type, const char *name, const char *issuer, const char *secretBase32, uint8_t digits, uint32_t period, uint8_t algorithm, uint64_t counter, uint8_t flags=0)
Updates an existing OATH entry.
static constexpr uint32_t DEFAULT_PERIOD
uint8_t timeRemaining(uint32_t period) const
Returns seconds remaining in current TOTP time step.
void setSlotRange(const cdc::core::IModule::SlotRange &range)
Configures logical-to-physical slot mapping for OATH entries.
static constexpr uint8_t ISSUER_LEN
bool findByName(const char *name, uint16_t *slotOut) const
Finds a logical slot index by account name.
bool readAccount(uint16_t slot, OathEntry *out)
Reads one OATH entry from secure-element storage.
bool deleteAccount(uint16_t slot)
Deletes account in logical slot.
ISecureElement * getSecureElementInstance()
Returns singleton secure-element stub instance.
Per-entry flag bits stored in OathEntry::flags.
constexpr uint8_t USB_CR_SLOT
Designate this CR entry as the USB OTP-HID slot-2 responder.
constexpr uint8_t TOUCH_REQUIRED
Require an on-device touch confirmation before answering a CR request.
static bool writePayload(uint16_t physSlot, const char *name, uint8_t type, const char *issuer, const uint8_t *secret, uint8_t secretLen, uint8_t digits, uint32_t period, uint64_t counter, uint8_t algorithm, uint8_t flags)
Builds and persists an OATH payload to a physical slot.
static bool validateOathParams(uint8_t type, uint8_t &digits, uint32_t &period, uint8_t &algorithm)
Validates OATH entry parameters and clamps to defaults when invalid.
static constexpr uint8_t OATH_DIGITS_MIN
Allowed OATH digit count range per RFC 4226 / RFC 6238.
static constexpr uint8_t OATH_DIGITS_MAX
static int base32CharValue(char c)
Converts one Base32 character into 5-bit value.
static constexpr uint32_t TOTP_PERIOD_MAX
static int base32Decode(const char *encoded, uint8_t *out, size_t outMax)
Decodes Base32 secret into raw bytes.
static constexpr uint32_t TOTP_PERIOD_MIN
Allowed TOTP period range in seconds.
static const uint32_t POWERS_10[]
static void formatCode(uint32_t code, uint8_t digits, char *codeOut, size_t codeOutLen)
Formats a numeric code into codeOut with leading zeros.
OathAlgorithm
Hash algorithm used by an OATH entry's HMAC engine.
Unified OATH credential record (TOTP, HOTP, and reserved CR).
uint8_t digits
Output digit count (TOTP/HOTP).
uint8_t secret[64]
Raw HMAC key.
uint8_t algorithm
OathAlgorithm value.
uint8_t secretLen
Valid bytes in secret.
uint64_t counter
Moving factor (HOTP only).
uint8_t type
OathType discriminator.
uint32_t period
TOTP step in seconds (TOTP only).
char issuer[32+1]
Optional issuer text.
char name[16+1]
Account label.
uint8_t flags
Reserved entry flags.
uint8_t secret[OathStore::SECRET_LEN]
char issuer[OathStore::ISSUER_LEN]