Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package libcotp for openSUSE:Factory checked 
in at 2026-04-16 19:25:09
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/libcotp (Old)
 and      /work/SRC/openSUSE:Factory/.libcotp.new.11940 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "libcotp"

Thu Apr 16 19:25:09 2026 rev:17 rq:1347296 version:4.0.1

Changes:
--------
--- /work/SRC/openSUSE:Factory/libcotp/libcotp.changes  2026-03-04 
21:10:11.844795584 +0100
+++ /work/SRC/openSUSE:Factory/.libcotp.new.11940/libcotp.changes       
2026-04-16 19:25:09.567760686 +0200
@@ -1,0 +2,19 @@
+Thu Apr 16 13:58:55 UTC 2026 - Paolo Stivanin <[email protected]>
+
+- Update to 4.0.1:
+  Security Fixes
+    * Fixed timing side-channel in validate_totp_in_window: comparison now 
uses min(gen_len, user_len) bytes to prevent reading past buffer bounds when 
lengths differ
+    * Normalized secret key is now zeroed with cotp_secure_memzero before 
freeing in compute_hmac
+    * Fixed memory leak in OpenSSL backend: EVP_MAC not freed when calloc 
fails in whmac_gethandle
+    * Fixed memory leak in OpenSSL backend: EVP_MAC_CTX not freed in 
whmac_freehandle and on buffer-too-small error path in whmac_finalize
+    * Added negative algorithm index validation (algo < 0) in all three HMAC 
backends (gcrypt, OpenSSL, MbedTLS)
+  Hardening
+    * All public symbols now use explicit 
__attribute__((visibility("default"))); library compiled with 
-fvisibility=hidden to minimize exported symbol surface
+    * Added linker hardening flags: full RELRO (-Wl,-z,relro,-z,now) and 
non-executable stack (-Wl,-z,noexecstack)
+    * REVERSE_BYTES macro wrapped in do { ... } while (0) for safe use in all 
statement contexts
+    * CMake now detects explicit_bzero at configure time via 
check_function_exists
+  Improvements
+    * Base32 validation (valid_b32_str) now enforces RFC 4648 padding rules: 
rejects data characters after padding, validates padding count (0, 1, 3, 4, or 
6), and requires padded strings to have length divisible by 8
+    * check_input max_len parameter changed from int32_t to size_t for type 
correctness
+
+-------------------------------------------------------------------

Old:
----
  v4.0.0.tar.gz
  v4.0.0.tar.gz.asc

New:
----
  v4.0.1.tar.gz
  v4.0.1.tar.gz.asc

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ libcotp.spec ++++++
--- /var/tmp/diff_new_pack.8b0HBl/_old  2026-04-16 19:25:10.463797621 +0200
+++ /var/tmp/diff_new_pack.8b0HBl/_new  2026-04-16 19:25:10.467797786 +0200
@@ -24,7 +24,7 @@
 
 %define libsoname %{name}4
 Name:           libcotp
-Version:        4.0.0
+Version:        4.0.1
 Release:        0
 Summary:        C library for generating TOTP and HOTP
 License:        Apache-2.0


++++++ v4.0.0.tar.gz -> v4.0.1.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libcotp-4.0.0/CMakeLists.txt 
new/libcotp-4.0.1/CMakeLists.txt
--- old/libcotp-4.0.0/CMakeLists.txt    2026-02-24 11:20:31.000000000 +0100
+++ new/libcotp-4.0.1/CMakeLists.txt    2026-04-15 16:33:19.000000000 +0200
@@ -1,5 +1,5 @@
 cmake_minimum_required(VERSION 3.16)
-project(cotp VERSION "4.0.0" LANGUAGES "C")
+project(cotp VERSION "4.0.1" LANGUAGES "C")
 
 set(CMAKE_C_STANDARD 11)
 
@@ -7,6 +7,9 @@
 
 include(GNUInstallDirs)
 include(CMakePackageConfigHelpers)
+include(CheckFunctionExists)
+
+check_function_exists(explicit_bzero HAVE_EXPLICIT_BZERO)
 
 find_package(PkgConfig)
 
@@ -63,6 +66,10 @@
 
 add_library(cotp ${SOURCE_FILES})
 
+if (HAVE_EXPLICIT_BZERO)
+    target_compile_definitions(cotp PRIVATE HAVE_EXPLICIT_BZERO)
+endif()
+
 if (COTP_ENABLE_VALIDATION)
     target_compile_definitions(cotp PUBLIC COTP_ENABLE_VALIDATION)
 endif()
@@ -78,7 +85,10 @@
 target_compile_options(cotp PRIVATE
         -Wall -Wextra -O3 -Wformat=2 -Wmissing-format-attribute 
-fstack-protector-strong -Wundef -Wmissing-format-attribute
         -fdiagnostics-color=always -Wstrict-prototypes -Wunreachable-code 
-Wchar-subscripts -Wwrite-strings -Wpointer-arith -Wbad-function-cast
-        -Wcast-align -Werror=format-security 
-Werror=implicit-function-declaration -Wno-sign-compare -Wno-format-nonliteral 
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3)
+        -Wcast-align -Werror=format-security 
-Werror=implicit-function-declaration -Wno-sign-compare -Wno-format-nonliteral 
-U_FORTIFY_SOURCE -D_FORTIFY_SOURCE=3
+        -fvisibility=hidden)
+
+target_link_options(cotp PRIVATE -Wl,-z,relro,-z,now -Wl,-z,noexecstack)
 
 set_target_properties(cotp PROPERTIES VERSION ${CMAKE_PROJECT_VERSION} 
SOVERSION ${CMAKE_PROJECT_VERSION_MAJOR})
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libcotp-4.0.0/README.md new/libcotp-4.0.1/README.md
--- old/libcotp-4.0.0/README.md 2026-02-24 11:20:31.000000000 +0100
+++ new/libcotp-4.0.1/README.md 2026-04-15 16:33:19.000000000 +0200
@@ -71,7 +71,7 @@
 - `base32_secret`: Base32 encoded (may contain spaces). `NULL` is invalid.
 - `digits`: 4–10 inclusive
 - `period`: 1–120 seconds inclusive
-- `algo`: `SHA1`, `SHA256`, `SHA512`
+- `algo`: `COTP_SHA1`, `COTP_SHA256`, `COTP_SHA512`
 - `counter`: non-negative
 - `timestamp`: UNIX epoch seconds
 
@@ -93,7 +93,7 @@
 
 ```c
 cotp_error_t err;
-char *code = get_totp("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", 6, 30, SHA1, &err);
+char *code = get_totp("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ", 6, 30, COTP_SHA1, 
&err);
 if (!code) {
     // handle error
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libcotp-4.0.0/src/cotp.h new/libcotp-4.0.1/src/cotp.h
--- old/libcotp-4.0.0/src/cotp.h        2026-02-24 11:20:31.000000000 +0100
+++ new/libcotp-4.0.1/src/cotp.h        2026-04-15 16:33:19.000000000 +0200
@@ -2,6 +2,12 @@
 #include <stdint.h>
 #include <stdbool.h>
 
+#if defined(__GNUC__) || defined(__clang__)
+    #define COTP_API __attribute__((visibility("default")))
+#else
+    #define COTP_API
+#endif
+
 #define COTP_SHA1   0
 #define COTP_SHA256 1
 #define COTP_SHA512 2
@@ -40,7 +46,7 @@
  * Returns 1 if it matches for any offset in [-window, +window], 0 otherwise.
  * On success and match, sets matched_delta to the offset that matched (may be 
0). On general failure, returns 0 and sets err_code.
  */
-int validate_totp_in_window(const char* user_code,
+COTP_API int validate_totp_in_window(const char* user_code,
                             const char* base32_encoded_secret,
                             long        timestamp,
                             int         digits,
@@ -52,10 +58,10 @@
 #endif
 
 // Context helpers
-cotp_ctx* cotp_ctx_create(int digits, int period, int sha_algo);
-void      cotp_ctx_free(cotp_ctx* ctx);
-char*     cotp_ctx_totp_at(cotp_ctx* ctx, const char* base32_encoded_secret, 
long timestamp, cotp_error_t* err);
-char*     cotp_ctx_totp(cotp_ctx* ctx, const char* base32_encoded_secret, 
cotp_error_t* err);
+COTP_API cotp_ctx* cotp_ctx_create(int digits, int period, int sha_algo);
+COTP_API void      cotp_ctx_free(cotp_ctx* ctx);
+COTP_API char*     cotp_ctx_totp_at(cotp_ctx* ctx, const char* 
base32_encoded_secret, long timestamp, cotp_error_t* err);
+COTP_API char*     cotp_ctx_totp(cotp_ctx* ctx, const char* 
base32_encoded_secret, cotp_error_t* err);
 
 /**
  * base32_encode
@@ -63,7 +69,7 @@
  * Ownership: returns a newly allocated, NUL-terminated string on success; 
caller must free() it.
  * On error: returns NULL and sets err_code.
  */
-char    *base32_encode     (const uint8_t *user_data,
+COTP_API char    *base32_encode     (const uint8_t *user_data,
                             size_t        data_len,
                             cotp_error_t *err_code);
 
@@ -74,7 +80,7 @@
  * The returned data preserves the input NUL when the original encoded content 
represented it.
  * On error: returns NULL and sets err_code.
  */
-uint8_t *base32_decode     (const char   *user_data_untrimmed,
+COTP_API uint8_t *base32_decode     (const char   *user_data_untrimmed,
                             size_t        data_len,
                             cotp_error_t *err_code);
 
@@ -83,7 +89,7 @@
  *
  * Checks whether a string is valid Base32 (ignoring ASCII spaces). Does not 
allocate.
  */
-bool     is_string_valid_b32 (const char *user_data);
+COTP_API bool     is_string_valid_b32 (const char *user_data);
 
 /**
  * get_hotp
@@ -91,7 +97,7 @@
  * Ownership: returns a newly allocated, zero-padded OTP string of requested 
width; caller must free().
  * On error: returns NULL and sets err_code.
  */
-char    *get_hotp          (const char   *base32_encoded_secret,
+COTP_API char    *get_hotp          (const char   *base32_encoded_secret,
                             long          counter,
                             int           digits,
                             int           sha_algo,
@@ -103,7 +109,7 @@
  * Ownership: returns a newly allocated, zero-padded OTP string; caller must 
free().
  * On error: returns NULL and sets err_code.
  */
-char    *get_totp          (const char   *base32_encoded_secret,
+COTP_API char    *get_totp          (const char   *base32_encoded_secret,
                             int           digits,
                             int           period,
                             int           sha_algo,
@@ -115,7 +121,7 @@
  * Ownership: returns a newly allocated Steam-style OTP string; caller must 
free().
  * On error: returns NULL and sets err_code.
  */
-char    *get_steam_totp    (const char   *base32_encoded_secret,
+COTP_API char    *get_steam_totp    (const char   *base32_encoded_secret,
                             int          period,
                             cotp_error_t *err_code);
 
@@ -125,7 +131,7 @@
  * Ownership: returns a newly allocated, zero-padded OTP string; caller must 
free().
  * On error: returns NULL and sets err_code.
  */
-char    *get_totp_at       (const char   *base32_encoded_secret,
+COTP_API char    *get_totp_at       (const char   *base32_encoded_secret,
                             long          time,
                             int           digits,
                             int           period,
@@ -138,7 +144,7 @@
  * Ownership: returns a newly allocated Steam-style OTP string; caller must 
free().
  * On error: returns NULL and sets err_code.
  */
-char    *get_steam_totp_at (const char   *base32_encoded_secret,
+COTP_API char    *get_steam_totp_at (const char   *base32_encoded_secret,
                             long          timestamp,
                             int           period,
                             cotp_error_t *err_code);
@@ -150,7 +156,7 @@
  * the returned integer will naturally drop them; err_code is set to 
MISSING_LEADING_ZERO in that case.
  * On invalid input returns -1 and sets err_code to INVALID_USER_INPUT.
  */
-int64_t  otp_to_int        (const char   *otp,
+COTP_API int64_t  otp_to_int        (const char   *otp,
                             cotp_error_t *err_code);
 
 #ifdef __cplusplus
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libcotp-4.0.0/src/otp.c new/libcotp-4.0.1/src/otp.c
--- old/libcotp-4.0.0/src/otp.c 2026-02-24 11:20:31.000000000 +0100
+++ new/libcotp-4.0.1/src/otp.c 2026-04-15 16:33:19.000000000 +0200
@@ -21,14 +21,18 @@
 
 #if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
 #define REVERSE_BYTES(C, C_reverse_byte_order)           \
-        for (int j = 0, i = 7; j < 8; j++, i--) {            \
+    do {                                                     \
+        for (int j = 0, i = 7; j < 8; j++, i--) {           \
             (C_reverse_byte_order)[i] = ((unsigned char *)&(C))[j]; \
-        }
+        }                                                    \
+    } while (0)
 #elif __BYTE_ORDER__ == __ORDER_BIG_ENDIAN__
 #define REVERSE_BYTES(C, C_reverse_byte_order)           \
+    do {                                                     \
         for (int j = 0; j < 8; j++) {                        \
             (C_reverse_byte_order)[j] = ((unsigned char *)&(C))[j]; \
-        }
+        }                                                    \
+    } while (0)
 #else
     #error "Unknown endianness"
 #endif
@@ -374,6 +378,7 @@
     }
 
     if (normalized_K[0] == '\0') {
+        cotp_secure_memzero(normalized_K, 1);
         free(normalized_K);
         *err_code = EMPTY_STRING;
         return NULL;
@@ -381,7 +386,9 @@
 
     size_t secret_len = b32_decoded_len_from_str(normalized_K);
 
-    unsigned char *secret = base32_decode (normalized_K, strlen(normalized_K), 
err_code);
+    size_t normalized_K_len = strlen(normalized_K);
+    unsigned char *secret = base32_decode (normalized_K, normalized_K_len, 
err_code);
+    cotp_secure_memzero(normalized_K, normalized_K_len);
     free (normalized_K);
     if (secret == NULL) {
         return NULL;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libcotp-4.0.0/src/utils/base32.c 
new/libcotp-4.0.1/src/utils/base32.c
--- old/libcotp-4.0.0/src/utils/base32.c        2026-02-24 11:20:31.000000000 
+0100
+++ new/libcotp-4.0.1/src/utils/base32.c        2026-04-15 16:33:19.000000000 
+0200
@@ -50,7 +50,7 @@
 
 static cotp_error_t  check_input    (const uint8_t *user_data,
                                      size_t         data_len,
-                                     int32_t        max_len);
+                                     size_t         max_len);
 
 static int           strip_char     (char          *str);
 
@@ -225,14 +225,37 @@
         return false;
     }
 
+    size_t len = 0;
+    size_t pad_count = 0;
+    bool seen_pad = false;
+
     while (*str) {
         uint8_t c = (uint8_t)*str;
         if (c >= 128 || !b32_valid[c]) {
             return false;
         }
+        if (c == '=') {
+            seen_pad = true;
+            pad_count++;
+        } else if (seen_pad) {
+            // data character after padding is invalid per RFC 4648
+            return false;
+        }
+        len++;
         str++;
     }
 
+    // If padding is present, validate count: valid padding counts are 0, 1, 
3, 4, 6
+    if (seen_pad) {
+        if (pad_count == 2 || pad_count == 5 || pad_count == 7 || pad_count > 
6) {
+            return false;
+        }
+        // total length (data + padding) must be a multiple of 8
+        if (len % 8 != 0) {
+            return false;
+        }
+    }
+
     return true;
 }
 
@@ -290,7 +313,7 @@
 static cotp_error_t
 check_input (const uint8_t *user_data,
              size_t         data_len,
-             int32_t        max_len)
+             size_t         max_len)
 {
     if (!user_data || data_len > max_len) {
         return INVALID_USER_INPUT;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libcotp-4.0.0/src/utils/secure_zero.h 
new/libcotp-4.0.1/src/utils/secure_zero.h
--- old/libcotp-4.0.0/src/utils/secure_zero.h   2026-02-24 11:20:31.000000000 
+0100
+++ new/libcotp-4.0.1/src/utils/secure_zero.h   2026-04-15 16:33:19.000000000 
+0200
@@ -1,15 +1,21 @@
 #pragma once
 #include <stddef.h>
 
+#if defined(__GNUC__) || defined(__clang__)
+    #define COTP_INTERNAL_API __attribute__((visibility("default")))
+#else
+    #define COTP_INTERNAL_API
+#endif
+
 #ifdef __cplusplus
 extern "C" {
 #endif
 
 // Wipes memory in a way that compilers must not elide. Use for secrets and 
HMAC outputs.
-void cotp_secure_memzero(void *ptr, size_t len);
+COTP_INTERNAL_API void cotp_secure_memzero(void *ptr, size_t len);
 
 // Constant-time comparison. Returns 0 if equal, non-zero otherwise.
-int cotp_timing_safe_memcmp(const void *a, const void *b, size_t len);
+COTP_INTERNAL_API int cotp_timing_safe_memcmp(const void *a, const void *b, 
size_t len);
 
 #ifdef __cplusplus
 }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libcotp-4.0.0/src/utils/validation.c 
new/libcotp-4.0.1/src/utils/validation.c
--- old/libcotp-4.0.0/src/utils/validation.c    2026-02-24 11:20:31.000000000 
+0100
+++ new/libcotp-4.0.1/src/utils/validation.c    2026-04-15 16:33:19.000000000 
+0200
@@ -36,7 +36,9 @@
             return 0;
         }
         size_t gen_len = strlen(gen);
-        int ok = (gen_len == user_len) && (cotp_timing_safe_memcmp(gen, 
user_code, gen_len) == 0);
+        int len_match = (gen_len == user_len);
+        int cmp_match = (cotp_timing_safe_memcmp(gen, user_code, gen_len < 
user_len ? gen_len : user_len) == 0);
+        int ok = len_match && cmp_match;
         free(gen);
         if (ok) {
             if (matched_delta) *matched_delta = delta;
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libcotp-4.0.0/src/utils/whmac_gcrypt.c 
new/libcotp-4.0.1/src/utils/whmac_gcrypt.c
--- old/libcotp-4.0.0/src/utils/whmac_gcrypt.c  2026-02-24 11:20:31.000000000 
+0100
+++ new/libcotp-4.0.1/src/utils/whmac_gcrypt.c  2026-04-15 16:33:19.000000000 
+0200
@@ -39,7 +39,7 @@
 
     whmac_handle_t *whmac_handle = NULL;
     gcry_md_hd_t hd;
-    if (algo > 2) {
+    if (algo < 0 || algo > 2) {
         return NULL;
     }
     gpg_error_t gpg_err = gcry_md_open (&hd, gcrypt_algo[algo], 
GCRY_MD_FLAG_HMAC);
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libcotp-4.0.0/src/utils/whmac_mbedtls.c 
new/libcotp-4.0.1/src/utils/whmac_mbedtls.c
--- old/libcotp-4.0.0/src/utils/whmac_mbedtls.c 2026-02-24 11:20:31.000000000 
+0100
+++ new/libcotp-4.0.1/src/utils/whmac_mbedtls.c 2026-04-15 16:33:19.000000000 
+0200
@@ -39,7 +39,7 @@
         return NULL;
     }
 
-    if (algo > 2) {
+    if (algo < 0 || algo > 2) {
         free (whmac_handle);
         return NULL;
     }
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libcotp-4.0.0/src/utils/whmac_openssl.c 
new/libcotp-4.0.1/src/utils/whmac_openssl.c
--- old/libcotp-4.0.0/src/utils/whmac_openssl.c 2026-02-24 11:20:31.000000000 
+0100
+++ new/libcotp-4.0.1/src/utils/whmac_openssl.c 2026-04-15 16:33:19.000000000 
+0200
@@ -37,7 +37,7 @@
     };
 
     whmac_handle_t *whmac_handle = NULL;
-    if (algo > 2) {
+    if (algo < 0 || algo > 2) {
         return NULL;
     }
 
@@ -45,6 +45,7 @@
     if (mac != NULL) {
         whmac_handle = calloc (1, sizeof(*whmac_handle));
         if (whmac_handle == NULL) {
+            EVP_MAC_free (mac);
             return NULL;
         }
         whmac_handle->mac = mac;
@@ -62,6 +63,9 @@
 whmac_freehandle (whmac_handle_t *hd)
 {
     if (!hd) return;
+    if (hd->ctx) {
+        EVP_MAC_CTX_free (hd->ctx);
+    }
     EVP_MAC_free (hd->mac);
     free (hd);
 }
@@ -103,6 +107,8 @@
     }
 
     if (dlen > buflen) {
+        EVP_MAC_CTX_free (hd->ctx);
+        hd->ctx = NULL;
         return -MEMORY_ALLOCATION_ERROR;
     }
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libcotp-4.0.0/tests/CMakeLists.txt 
new/libcotp-4.0.1/tests/CMakeLists.txt
--- old/libcotp-4.0.0/tests/CMakeLists.txt      2026-02-24 11:20:31.000000000 
+0100
+++ new/libcotp-4.0.1/tests/CMakeLists.txt      2026-04-15 16:33:19.000000000 
+0200
@@ -12,3 +12,9 @@
 add_test (NAME TestBase32Encode COMMAND test_base32encode)
 add_test (NAME TestBase32Decode COMMAND test_base32decode)
 add_test (NAME TestBase32Roundtrip COMMAND test_base32_roundtrip)
+
+if (COTP_ENABLE_VALIDATION)
+    add_executable (test_validation test_validation.c)
+    target_link_libraries (test_validation PRIVATE cotp criterion)
+    add_test (NAME TestValidation COMMAND test_validation)
+endif()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libcotp-4.0.0/tests/test_base32decode.c 
new/libcotp-4.0.1/tests/test_base32decode.c
--- old/libcotp-4.0.0/tests/test_base32decode.c 2026-02-24 11:20:31.000000000 
+0100
+++ new/libcotp-4.0.1/tests/test_base32decode.c 2026-04-15 16:33:19.000000000 
+0200
@@ -125,4 +125,42 @@
     }
 
     free (binary);
+}
+
+
+Test(b32_decode_test, b32_padding_in_middle) {
+    cotp_error_t err;
+    const char *k = "MY=W======";
+
+    uint8_t *dk = base32_decode (k, strlen(k), &err);
+
+    cr_expect_null (dk, "Expected NULL for padding in middle of string");
+    cr_expect_eq (err, INVALID_B32_INPUT);
+}
+
+
+Test(b32_decode_test, b32_invalid_padding_count) {
+    cotp_error_t err;
+    // 2 padding chars is never valid in base32
+    const char *k = "MZXW6Y==";
+
+    uint8_t *dk = base32_decode (k, strlen(k), &err);
+
+    cr_expect_null (dk, "Expected NULL for invalid padding count");
+    cr_expect_eq (err, INVALID_B32_INPUT);
+}
+
+
+Test(b32_decode_test, b32_no_padding_valid) {
+    cotp_error_t err;
+    // No padding at all is accepted (common for OTP secrets)
+    const char *k = "MZXW6YTBOI";
+    const char *expected = "foobar";
+
+    uint8_t *dk = base32_decode (k, strlen(k), &err);
+
+    cr_expect_eq (err, NO_ERROR);
+    cr_expect_str_eq ((char *)dk, expected, "Expected %s to be equal to %s", 
dk, expected);
+
+    free (dk);
 }
\ No newline at end of file
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libcotp-4.0.0/tests/test_otp.c 
new/libcotp-4.0.1/tests/test_otp.c
--- old/libcotp-4.0.0/tests/test_otp.c  2026-02-24 11:20:31.000000000 +0100
+++ new/libcotp-4.0.1/tests/test_otp.c  2026-04-15 16:33:19.000000000 +0200
@@ -31,8 +31,10 @@
 
     cotp_error_t err;
     for (int i = 0; i < 6; i++) {
-        int64_t totp = otp_to_int (get_totp_at (K_base32, counter[i], 8, 30, 
COTP_SHA1, &err), &err);
+        char *totp_str = get_totp_at (K_base32, counter[i], 8, 30, COTP_SHA1, 
&err);
+        int64_t totp = otp_to_int (totp_str, &err);
         cr_expect_eq (totp, expected_totp[i], "Expected %08ld to be equal to 
%08ld\n", totp, expected_totp[i]);
+        free (totp_str);
     }
     free (K_base32);
 }
@@ -63,9 +65,11 @@
     char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, 
&cotp_err);
 
     cotp_error_t err;
-    int64_t totp = otp_to_int (get_totp_at (K_base32, counter, 10, 30, 
COTP_SHA1, &err), &err);
+    char *totp_str = get_totp_at (K_base32, counter, 10, 30, COTP_SHA1, &err);
+    int64_t totp = otp_to_int (totp_str, &err);
     cr_expect_eq (totp, expected_totp, "Expected %010ld to be equal to 
%010ld\n", totp, expected_totp);
 
+    free (totp_str);
     free (K_base32);
 }
 
@@ -274,9 +278,11 @@
     char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, 
&cotp_err);
 
     cotp_error_t err;
-    int64_t totp = otp_to_int (get_totp_at (K_base32, counter, 10, 30, 
COTP_SHA1, &err), &err);
+    char *totp_str = get_totp_at (K_base32, counter, 10, 30, COTP_SHA1, &err);
+    int64_t totp = otp_to_int (totp_str, &err);
     cr_expect_eq (err, MISSING_LEADING_ZERO, "Expected %d to be equal to 
%d\n", err, MISSING_LEADING_ZERO);
 
+    free (totp_str);
     free (K_base32);
 }
 
@@ -312,3 +318,214 @@
     cr_expect_eq (err, INVALID_USER_INPUT, "Expected %d to be equal to %d\n", 
err, INVALID_USER_INPUT);
     cr_assert_null (totp);
 }
+
+
+Test(totp_generic, test_empty_secret) {
+    cotp_error_t err;
+    char *totp = get_totp ("", 6, 30, COTP_SHA1, &err);
+
+    cr_expect_eq (err, EMPTY_STRING, "Expected %d to be equal to %d\n", err, 
EMPTY_STRING);
+    cr_assert_null (totp);
+}
+
+
+Test(totp_boundary, test_min_digits) {
+    const char *K = "12345678901234567890";
+
+    cotp_error_t cotp_err;
+    char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, 
&cotp_err);
+
+    cotp_error_t err;
+    char *totp = get_totp_at (K_base32, 59, 4, 30, COTP_SHA1, &err);
+    cr_expect_eq (err, NO_ERROR, "Expected %d to be equal to %d\n", err, 
NO_ERROR);
+    cr_assert_not_null (totp);
+    cr_expect_eq (strlen(totp), 4, "Expected length 4, got %zu\n", 
strlen(totp));
+
+    free (totp);
+    free (K_base32);
+}
+
+
+Test(totp_boundary, test_max_digits) {
+    const char *K = "12345678901234567890";
+    const char *expected_totp = "0689005924";
+
+    cotp_error_t cotp_err;
+    char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, 
&cotp_err);
+
+    cotp_error_t err;
+    char *totp = get_totp_at (K_base32, 1234567890, 10, 30, COTP_SHA1, &err);
+    cr_expect_eq (err, NO_ERROR, "Expected %d to be equal to %d\n", err, 
NO_ERROR);
+    cr_assert_not_null (totp);
+    cr_expect_eq (strlen(totp), 10, "Expected length 10, got %zu\n", 
strlen(totp));
+    cr_expect_str_eq (totp, expected_totp, "Expected %s to be equal to %s\n", 
totp, expected_totp);
+
+    free (totp);
+    free (K_base32);
+}
+
+
+Test(totp_boundary, test_min_period) {
+    const char *K = "12345678901234567890";
+
+    cotp_error_t cotp_err;
+    char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, 
&cotp_err);
+
+    cotp_error_t err;
+    char *totp = get_totp_at (K_base32, 59, 6, 1, COTP_SHA1, &err);
+    cr_expect_eq (err, NO_ERROR, "Expected %d to be equal to %d\n", err, 
NO_ERROR);
+    cr_assert_not_null (totp);
+
+    free (totp);
+    free (K_base32);
+}
+
+
+Test(totp_boundary, test_max_period) {
+    const char *K = "12345678901234567890";
+
+    cotp_error_t cotp_err;
+    char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, 
&cotp_err);
+
+    cotp_error_t err;
+    char *totp = get_totp_at (K_base32, 59, 6, 120, COTP_SHA1, &err);
+    cr_expect_eq (err, NO_ERROR, "Expected %d to be equal to %d\n", err, 
NO_ERROR);
+    cr_assert_not_null (totp);
+
+    free (totp);
+    free (K_base32);
+}
+
+
+Test(totp_boundary, test_period_over_max) {
+    const char *K = "12345678901234567890";
+
+    cotp_error_t cotp_err;
+    char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, 
&cotp_err);
+
+    cotp_error_t err;
+    char *totp = get_totp_at (K_base32, 59, 6, 121, COTP_SHA1, &err);
+    cr_expect_eq (err, INVALID_PERIOD, "Expected %d to be equal to %d\n", err, 
INVALID_PERIOD);
+    cr_assert_null (totp);
+
+    free (K_base32);
+}
+
+
+Test(totp_boundary, test_digits_below_min) {
+    const char *K = "12345678901234567890";
+
+    cotp_error_t cotp_err;
+    char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, 
&cotp_err);
+
+    cotp_error_t err;
+    char *totp = get_totp_at (K_base32, 59, 3, 30, COTP_SHA1, &err);
+    cr_expect_eq (err, INVALID_DIGITS, "Expected %d to be equal to %d\n", err, 
INVALID_DIGITS);
+    cr_assert_null (totp);
+
+    free (K_base32);
+}
+
+
+Test(totp_boundary, test_digits_above_max) {
+    const char *K = "12345678901234567890";
+
+    cotp_error_t cotp_err;
+    char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, 
&cotp_err);
+
+    cotp_error_t err;
+    char *totp = get_totp_at (K_base32, 59, 11, 30, COTP_SHA1, &err);
+    cr_expect_eq (err, INVALID_DIGITS, "Expected %d to be equal to %d\n", err, 
INVALID_DIGITS);
+    cr_assert_null (totp);
+
+    free (K_base32);
+}
+
+
+// Context API tests (T2)
+Test(ctx_api, test_create_valid) {
+    cotp_ctx *ctx = cotp_ctx_create (6, 30, COTP_SHA1);
+    cr_assert_not_null (ctx);
+    cotp_ctx_free (ctx);
+}
+
+
+Test(ctx_api, test_create_all_algos) {
+    cotp_ctx *ctx1 = cotp_ctx_create (6, 30, COTP_SHA1);
+    cotp_ctx *ctx2 = cotp_ctx_create (6, 30, COTP_SHA256);
+    cotp_ctx *ctx3 = cotp_ctx_create (6, 30, COTP_SHA512);
+    cr_assert_not_null (ctx1);
+    cr_assert_not_null (ctx2);
+    cr_assert_not_null (ctx3);
+    cotp_ctx_free (ctx1);
+    cotp_ctx_free (ctx2);
+    cotp_ctx_free (ctx3);
+}
+
+
+Test(ctx_api, test_create_invalid_digits) {
+    cotp_ctx *ctx = cotp_ctx_create (3, 30, COTP_SHA1);
+    cr_assert_null (ctx);
+
+    ctx = cotp_ctx_create (11, 30, COTP_SHA1);
+    cr_assert_null (ctx);
+}
+
+
+Test(ctx_api, test_create_invalid_period) {
+    cotp_ctx *ctx = cotp_ctx_create (6, 0, COTP_SHA1);
+    cr_assert_null (ctx);
+
+    ctx = cotp_ctx_create (6, -1, COTP_SHA1);
+    cr_assert_null (ctx);
+
+    ctx = cotp_ctx_create (6, 121, COTP_SHA1);
+    cr_assert_null (ctx);
+}
+
+
+Test(ctx_api, test_create_invalid_algo) {
+    cotp_ctx *ctx = cotp_ctx_create (6, 30, 3);
+    cr_assert_null (ctx);
+
+    ctx = cotp_ctx_create (6, 30, -1);
+    cr_assert_null (ctx);
+}
+
+
+Test(ctx_api, test_totp_at) {
+    const char *K = "12345678901234567890";
+    const char *expected_totp = "94287082";
+
+    cotp_error_t cotp_err;
+    char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, 
&cotp_err);
+
+    cotp_ctx *ctx = cotp_ctx_create (8, 30, COTP_SHA1);
+    cr_assert_not_null (ctx);
+
+    cotp_error_t err;
+    char *totp = cotp_ctx_totp_at (ctx, K_base32, 59, &err);
+    cr_expect_eq (err, NO_ERROR, "Expected %d to be equal to %d\n", err, 
NO_ERROR);
+    cr_expect_str_eq (totp, expected_totp, "Expected %s to be equal to %s\n", 
totp, expected_totp);
+
+    free (totp);
+    cotp_ctx_free (ctx);
+    free (K_base32);
+}
+
+
+Test(ctx_api, test_null_ctx) {
+    cotp_error_t err;
+    char *totp = cotp_ctx_totp_at (NULL, "secret", 59, &err);
+    cr_expect_eq (err, INVALID_USER_INPUT, "Expected %d to be equal to %d\n", 
err, INVALID_USER_INPUT);
+    cr_assert_null (totp);
+
+    totp = cotp_ctx_totp (NULL, "secret", &err);
+    cr_expect_eq (err, INVALID_USER_INPUT, "Expected %d to be equal to %d\n", 
err, INVALID_USER_INPUT);
+    cr_assert_null (totp);
+}
+
+
+Test(ctx_api, test_free_null) {
+    cotp_ctx_free (NULL);
+}
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/libcotp-4.0.0/tests/test_validation.c 
new/libcotp-4.0.1/tests/test_validation.c
--- old/libcotp-4.0.0/tests/test_validation.c   1970-01-01 01:00:00.000000000 
+0100
+++ new/libcotp-4.0.1/tests/test_validation.c   2026-04-15 16:33:19.000000000 
+0200
@@ -0,0 +1,101 @@
+#include <criterion/criterion.h>
+#include <string.h>
+#include "../src/cotp.h"
+
+#ifdef COTP_ENABLE_VALIDATION
+
+Test(validation, test_exact_match) {
+    const char *K = "12345678901234567890";
+
+    cotp_error_t cotp_err;
+    char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, 
&cotp_err);
+
+    cotp_error_t err;
+    char *totp = get_totp_at (K_base32, 59, 8, 30, COTP_SHA1, &err);
+    cr_assert_not_null (totp);
+
+    int matched_delta = -999;
+    int result = validate_totp_in_window (totp, K_base32, 59, 8, 30, 
COTP_SHA1, 1, &matched_delta, &err);
+    cr_expect_eq (result, 1, "Expected match\n");
+    cr_expect_eq (matched_delta, 0, "Expected delta 0, got %d\n", 
matched_delta);
+    cr_expect_eq (err, VALID, "Expected VALID, got %d\n", err);
+
+    free (totp);
+    free (K_base32);
+}
+
+
+Test(validation, test_no_match) {
+    const char *K = "12345678901234567890";
+
+    cotp_error_t cotp_err;
+    char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, 
&cotp_err);
+
+    cotp_error_t err;
+    int matched_delta = -999;
+    int result = validate_totp_in_window ("00000000", K_base32, 59, 8, 30, 
COTP_SHA1, 0, &matched_delta, &err);
+    cr_expect_eq (result, 0, "Expected no match\n");
+
+    free (K_base32);
+}
+
+
+Test(validation, test_window_offset_match) {
+    const char *K = "12345678901234567890";
+
+    cotp_error_t cotp_err;
+    char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, 
&cotp_err);
+
+    // Generate TOTP for timestamp 59
+    cotp_error_t err;
+    char *totp = get_totp_at (K_base32, 59, 8, 30, COTP_SHA1, &err);
+    cr_assert_not_null (totp);
+
+    // Validate at timestamp 59+30=89 (one period later) with window=1 should 
still match at delta=-1
+    int matched_delta = -999;
+    int result = validate_totp_in_window (totp, K_base32, 89, 8, 30, 
COTP_SHA1, 1, &matched_delta, &err);
+    cr_expect_eq (result, 1, "Expected match within window\n");
+    cr_expect_eq (matched_delta, -1, "Expected delta -1, got %d\n", 
matched_delta);
+
+    free (totp);
+    free (K_base32);
+}
+
+
+Test(validation, test_null_user_code) {
+    cotp_error_t err;
+    int result = validate_totp_in_window (NULL, "secret", 59, 8, 30, 
COTP_SHA1, 1, NULL, &err);
+    cr_expect_eq (result, 0);
+    cr_expect_eq (err, INVALID_USER_INPUT);
+}
+
+
+Test(validation, test_null_secret) {
+    cotp_error_t err;
+    int result = validate_totp_in_window ("12345678", NULL, 59, 8, 30, 
COTP_SHA1, 1, NULL, &err);
+    cr_expect_eq (result, 0);
+    cr_expect_eq (err, INVALID_USER_INPUT);
+}
+
+
+Test(validation, test_negative_window) {
+    const char *K = "12345678901234567890";
+
+    cotp_error_t cotp_err;
+    char *K_base32 = base32_encode ((const uint8_t *)K, strlen(K)+1, 
&cotp_err);
+
+    cotp_error_t err;
+    char *totp = get_totp_at (K_base32, 59, 8, 30, COTP_SHA1, &err);
+    cr_assert_not_null (totp);
+
+    // Negative window should be normalized to positive
+    int matched_delta = -999;
+    int result = validate_totp_in_window (totp, K_base32, 59, 8, 30, 
COTP_SHA1, -2, &matched_delta, &err);
+    cr_expect_eq (result, 1, "Expected match with negative window\n");
+    cr_expect_eq (matched_delta, 0, "Expected delta 0, got %d\n", 
matched_delta);
+
+    free (totp);
+    free (K_base32);
+}
+
+#endif // COTP_ENABLE_VALIDATION

Reply via email to