From 1167bd97015529b6248b504b8864eb69d7dee89d Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Tue, 5 May 2026 18:27:19 +0300 Subject: [PATCH 1/3] Decompose utils from duox-related code --- client/src/crypto/duoxcrypto.c | 207 ++------------------------------- client/src/crypto/duoxcrypto.h | 1 - client/src/crypto/libpcrypto.c | 89 ++------------ client/src/fileutils.c | 167 +++++++++++++++++++++++++- client/src/fileutils.h | 36 ++++++ client/src/util.c | 72 ++++++++++++ client/src/util.h | 15 +++ 7 files changed, 304 insertions(+), 283 deletions(-) diff --git a/client/src/crypto/duoxcrypto.c b/client/src/crypto/duoxcrypto.c index ae7088131..56a36d25a 100644 --- a/client/src/crypto/duoxcrypto.c +++ b/client/src/crypto/duoxcrypto.c @@ -18,12 +18,9 @@ #include "crypto/duoxcrypto.h" -#include -#include #include #include #include -#include #include #include #include @@ -40,6 +37,7 @@ #include "x509_crt.h" #define DUOX_CERTIFICATE_ANCHOR_INPUT_LEN 8192 +#define DUOX_CERTIFICATE_ANCHOR_SEARCH_DEPTH 4 static int duox_cert_info_from_x509_crt(const mbedtls_x509_crt *cert, duox_cert_info_t *out); @@ -55,189 +53,6 @@ const char *duox_certificate_format_name(duox_certificate_format_t format) { } } -const char *duox_cert_info_format_name(const duox_cert_info_t *cert) { - if (cert == NULL) { - return duox_certificate_format_name(DUOX_CERTIFICATE_FORMAT_UNKNOWN); - } - return duox_certificate_format_name(cert->format); -} - -static void duox_trim_ascii_inplace(char *text) { - if (text == NULL) { - return; - } - - size_t start = 0; - size_t len = strlen(text); - while (start < len && isspace((unsigned char)text[start])) { - start++; - } - while (len > start && isspace((unsigned char)text[len - 1])) { - len--; - } - - if (start > 0) { - memmove(text, text + start, len - start); - } - text[len - start] = '\0'; -} - -static int duox_copy_without_whitespace(const char *src, char *dst, size_t dst_size, size_t *dst_len) { - if (src == NULL || dst == NULL || dst_len == NULL || dst_size == 0) { - return PM3_EINVARG; - } - - size_t out = 0; - for (size_t i = 0; src[i] != '\0'; i++) { - if (isspace((unsigned char)src[i])) { - continue; - } - if ((out + 1) >= dst_size) { - return PM3_EOVFLOW; - } - dst[out++] = src[i]; - } - dst[out] = '\0'; - *dst_len = out; - return PM3_SUCCESS; -} - -static bool duox_path_is_directory(const char *path) { - if (path == NULL) { - return false; - } - struct stat st; - if (stat(path, &st) != 0) { - return false; - } - return S_ISDIR(st.st_mode) != 0; -} - -static bool duox_path_is_regular_file(const char *path) { - if (path == NULL) { - return false; - } - struct stat st; - if (stat(path, &st) != 0) { - return false; - } - return S_ISREG(st.st_mode) != 0; -} - -static const char *duox_path_basename(const char *path) { - if (path == NULL) { - return ""; - } - - const char *base = strrchr(path, '/'); - const char *base_win = strrchr(path, '\\'); - if (base == NULL || (base_win != NULL && base_win > base)) { - base = base_win; - } - return (base == NULL) ? path : (base + 1); -} - -static void duox_path_basename_without_ext(const char *path, char *out, size_t out_len) { - if (out == NULL || out_len == 0) { - return; - } - out[0] = '\0'; - - const char *base = duox_path_basename(path); - if (base[0] == '\0') { - return; - } - - snprintf(out, out_len, "%s", base); - char *dot = strrchr(out, '.'); - if (dot != NULL && dot != out) { - *dot = '\0'; - } -} - -static int duox_qsort_path_cmp(const void *a, const void *b) { - const char *pa = (const char *)a; - const char *pb = (const char *)b; - return strcmp(pa, pb); -} - -static int duox_collect_certificate_anchor_paths_recursive(const char *dirpath, - char paths[][DUOX_CERTIFICATE_ANCHOR_PATH_LEN], - size_t max_paths, size_t *count) { - if (dirpath == NULL || paths == NULL || count == NULL) { - return PM3_EINVARG; - } - - DIR *dir = opendir(dirpath); - if (dir == NULL) { - return PM3_EFILE; - } - - struct dirent *entry = NULL; - while ((entry = readdir(dir)) != NULL) { - if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0 || entry->d_name[0] == '.') { - continue; - } - - char fullpath[DUOX_CERTIFICATE_ANCHOR_PATH_LEN] = {0}; - if (snprintf(fullpath, sizeof(fullpath), "%s/%s", dirpath, entry->d_name) >= (int)sizeof(fullpath)) { - continue; - } - - if (duox_path_is_directory(fullpath)) { - int res = duox_collect_certificate_anchor_paths_recursive(fullpath, paths, max_paths, count); - if (res != PM3_SUCCESS) { - closedir(dir); - return res; - } - continue; - } - - if (!duox_path_is_regular_file(fullpath)) { - continue; - } - if (*count >= max_paths) { - closedir(dir); - return PM3_EOVFLOW; - } - - snprintf(paths[*count], DUOX_CERTIFICATE_ANCHOR_PATH_LEN, "%s", fullpath); - (*count)++; - } - - closedir(dir); - return PM3_SUCCESS; -} - -static int duox_collect_certificate_anchor_paths(const char *anchor_store_dir, - char paths[][DUOX_CERTIFICATE_ANCHOR_PATH_LEN], - size_t max_paths, size_t *count) { - if (anchor_store_dir == NULL || anchor_store_dir[0] == '\0' || paths == NULL || count == NULL) { - return PM3_EINVARG; - } - - char *rootdir = NULL; - int res = searchFile(&rootdir, RESOURCES_SUBDIR, anchor_store_dir, "", true); - if (res != PM3_SUCCESS) { - return res; - } - - if (!duox_path_is_directory(rootdir)) { - free(rootdir); - return PM3_EFILE; - } - - *count = 0; - res = duox_collect_certificate_anchor_paths_recursive(rootdir, paths, max_paths, count); - free(rootdir); - if (res != PM3_SUCCESS) { - return res; - } - - qsort(paths, *count, sizeof(paths[0]), duox_qsort_path_cmp); - return PM3_SUCCESS; -} - int duox_certificate_anchor_public_key(const duox_certificate_anchor_t *anchor, mbedtls_ecp_group_id *curveid, const uint8_t **pubkey, size_t *pubkey_len) { @@ -299,7 +114,7 @@ static int duox_load_x509_certificate_input(const char *input, mbedtls_x509_crt return PM3_EOVFLOW; } memcpy(normalized, input, input_len + 1); - duox_trim_ascii_inplace(normalized); + str_trim_ascii_inplace(normalized); if (normalized[0] == '\0') { return PM3_EINVARG; } @@ -319,7 +134,7 @@ static int duox_load_x509_certificate_input(const char *input, mbedtls_x509_crt char compact[DUOX_CERTIFICATE_ANCHOR_INPUT_LEN] = {0}; size_t compact_len = 0; - if (duox_copy_without_whitespace(normalized, compact, sizeof(compact), &compact_len) != PM3_SUCCESS || compact_len == 0) { + if (str_copy_without_whitespace(normalized, compact, sizeof(compact), &compact_len) != PM3_SUCCESS || compact_len == 0) { return PM3_EINVARG; } @@ -425,7 +240,7 @@ static int duox_load_certificate_anchor_from_file_path(const char *filepath, duo } char filename_anchor_name[DUOX_CERTIFICATE_ANCHOR_NAME_LEN] = {0}; - duox_path_basename_without_ext(filepath, filename_anchor_name, sizeof(filename_anchor_name)); + path_basename_without_ext(filepath, filename_anchor_name, sizeof(filename_anchor_name)); if (filename_anchor_name[0] == '\0') { return PM3_EINVARG; } @@ -446,14 +261,15 @@ static int duox_load_named_certificate_anchor_from_store(const char *token, cons char paths[DUOX_CERTIFICATE_ANCHOR_MAX_PATHS][DUOX_CERTIFICATE_ANCHOR_PATH_LEN] = {{0}}; size_t path_count = 0; - int res = duox_collect_certificate_anchor_paths(anchor_store_dir, paths, ARRAYLEN(paths), &path_count); + int res = collect_resource_file_paths(anchor_store_dir, (char *)paths, sizeof(paths[0]), ARRAYLEN(paths), &path_count, + false, DUOX_CERTIFICATE_ANCHOR_SEARCH_DEPTH); if (res != PM3_SUCCESS) { return res; } for (size_t i = 0; i < path_count; i++) { char filename_anchor_name[DUOX_CERTIFICATE_ANCHOR_NAME_LEN] = {0}; - duox_path_basename_without_ext(paths[i], filename_anchor_name, sizeof(filename_anchor_name)); + path_basename_without_ext(paths[i], filename_anchor_name, sizeof(filename_anchor_name)); if (filename_anchor_name[0] == '\0') { continue; } @@ -466,7 +282,7 @@ static int duox_load_named_certificate_anchor_from_store(const char *token, cons const char *matched_path = NULL; for (size_t i = 0; i < path_count; i++) { char filename_anchor_name[DUOX_CERTIFICATE_ANCHOR_NAME_LEN] = {0}; - duox_path_basename_without_ext(paths[i], filename_anchor_name, sizeof(filename_anchor_name)); + path_basename_without_ext(paths[i], filename_anchor_name, sizeof(filename_anchor_name)); if (filename_anchor_name[0] == '\0' || !str_startswith_case_insensitive(filename_anchor_name, token)) { continue; } @@ -491,7 +307,7 @@ int duox_load_certificate_anchor_from_input(const char *input, const char *ancho char normalized[DUOX_CERTIFICATE_ANCHOR_INPUT_LEN] = {0}; snprintf(normalized, sizeof(normalized), "%s", input); - duox_trim_ascii_inplace(normalized); + str_trim_ascii_inplace(normalized); if (normalized[0] == '\0') { return PM3_EINVARG; } @@ -499,7 +315,7 @@ int duox_load_certificate_anchor_from_input(const char *input, const char *ancho char *resolved_path = NULL; if (searchFile(&resolved_path, RESOURCES_SUBDIR, normalized, "", true) == PM3_SUCCESS) { int res = PM3_EINVARG; - if (duox_path_is_regular_file(resolved_path)) { + if (path_is_regular_file(resolved_path)) { res = duox_load_certificate_anchor_from_file_path(resolved_path, anchor); } free(resolved_path); @@ -532,7 +348,8 @@ int duox_load_certificate_anchors_from_store(const char *anchor_store_dir, char paths[DUOX_CERTIFICATE_ANCHOR_MAX_PATHS][DUOX_CERTIFICATE_ANCHOR_PATH_LEN] = {{0}}; size_t path_count = 0; - int res = duox_collect_certificate_anchor_paths(anchor_store_dir, paths, ARRAYLEN(paths), &path_count); + int res = collect_resource_file_paths(anchor_store_dir, (char *)paths, sizeof(paths[0]), ARRAYLEN(paths), &path_count, + false, DUOX_CERTIFICATE_ANCHOR_SEARCH_DEPTH); if (res != PM3_SUCCESS) { return res; } diff --git a/client/src/crypto/duoxcrypto.h b/client/src/crypto/duoxcrypto.h index 4b492c142..76b9a9f32 100644 --- a/client/src/crypto/duoxcrypto.h +++ b/client/src/crypto/duoxcrypto.h @@ -76,7 +76,6 @@ typedef struct { } duox_certificate_anchor_t; const char *duox_certificate_format_name(duox_certificate_format_t format); -const char *duox_cert_info_format_name(const duox_cert_info_t *cert); int duox_certificate_anchor_public_key(const duox_certificate_anchor_t *anchor, mbedtls_ecp_group_id *curveid, const uint8_t **pubkey, size_t *pubkey_len); diff --git a/client/src/crypto/libpcrypto.c b/client/src/crypto/libpcrypto.c index 7844a41c9..6bb87072c 100644 --- a/client/src/crypto/libpcrypto.c +++ b/client/src/crypto/libpcrypto.c @@ -22,7 +22,6 @@ #include #include #include -#include #include #include #include @@ -106,78 +105,6 @@ int pcrypto_rng_fill_oneshot(uint8_t *out, size_t out_len, const char *personali return res; } -static void pcrypto_trim_ascii_inplace(char *text) { - if (text == NULL) { - return; - } - - size_t start = 0; - size_t len = strlen(text); - while (start < len && isspace((unsigned char)text[start])) { - start++; - } - while (len > start && isspace((unsigned char)text[len - 1])) { - len--; - } - - if (start > 0) { - memmove(text, text + start, len - start); - } - text[len - start] = '\0'; -} - -static void pcrypto_unescape_newlines_inplace(char *text) { - if (text == NULL) { - return; - } - - size_t read_pos = 0; - size_t write_pos = 0; - size_t len = strlen(text); - while (read_pos < len) { - if (text[read_pos] == '\\' && (read_pos + 1) < len) { - char esc = text[read_pos + 1]; - if (esc == 'n') { - text[write_pos++] = '\n'; - read_pos += 2; - continue; - } - if (esc == 'r') { - text[write_pos++] = '\r'; - read_pos += 2; - continue; - } - if (esc == 't') { - text[write_pos++] = '\t'; - read_pos += 2; - continue; - } - } - text[write_pos++] = text[read_pos++]; - } - text[write_pos] = '\0'; -} - -static int pcrypto_copy_without_whitespace(const char *src, char *dst, size_t dst_size, size_t *dst_len) { - if (src == NULL || dst == NULL || dst_len == NULL || dst_size == 0) { - return PM3_EINVARG; - } - - size_t out = 0; - for (size_t i = 0; src[i] != '\0'; i++) { - if (isspace((unsigned char)src[i])) { - continue; - } - if ((out + 1) >= dst_size) { - return PM3_EOVFLOW; - } - dst[out++] = src[i]; - } - dst[out] = '\0'; - *dst_len = out; - return PM3_SUCCESS; -} - static int pcrypto_extract_priv_scalar_from_pk(const mbedtls_pk_context *pkctx, mbedtls_ecp_group_id curveid, uint8_t *out_priv, size_t out_priv_len) { @@ -265,7 +192,7 @@ static int pcrypto_parse_ec_private_base64(const char *input, char compact[PCRYPTO_MAX_KEY_INPUT] = {0}; size_t compact_len = 0; - int res = pcrypto_copy_without_whitespace(input, compact, sizeof(compact), &compact_len); + int res = str_copy_without_whitespace(input, compact, sizeof(compact), &compact_len); if (res != PM3_SUCCESS || compact_len == 0) { return PM3_EINVARG; } @@ -399,7 +326,7 @@ static int pcrypto_parse_ec_private_text(const char *input, bool allow_file_path return PM3_EOVFLOW; } memcpy(normalized, input, input_len + 1); - pcrypto_trim_ascii_inplace(normalized); + str_trim_ascii_inplace(normalized); if (normalized[0] == '\0') { return PM3_EINVARG; @@ -416,13 +343,13 @@ static int pcrypto_parse_ec_private_text(const char *input, bool allow_file_path // Only unescape after path resolution fails, to avoid mutating valid paths // (for example Windows paths containing '\t', '\n' or '\r'). - pcrypto_unescape_newlines_inplace(normalized); + str_unescape_newlines_inplace(normalized); uint8_t decoded[PCRYPTO_MAX_KEY_INPUT] = {0}; int decoded_len = -1; char compact[PCRYPTO_MAX_KEY_INPUT] = {0}; size_t compact_len = 0; - if (pcrypto_copy_without_whitespace(normalized, compact, sizeof(compact), &compact_len) == PM3_SUCCESS && + if (str_copy_without_whitespace(normalized, compact, sizeof(compact), &compact_len) == PM3_SUCCESS && compact_len > 0) { decoded_len = hex_to_bytes(compact, decoded, sizeof(decoded)); } @@ -740,7 +667,7 @@ static int pcrypto_parse_ec_public_base64(const char *input, char compact[PCRYPTO_MAX_KEY_INPUT] = {0}; size_t compact_len = 0; - int res = pcrypto_copy_without_whitespace(input, compact, sizeof(compact), &compact_len); + int res = str_copy_without_whitespace(input, compact, sizeof(compact), &compact_len); if (res != PM3_SUCCESS || compact_len == 0) { return PM3_EINVARG; } @@ -777,7 +704,7 @@ static int pcrypto_parse_ec_public_text(const char *input, bool allow_file_path, return PM3_EOVFLOW; } memcpy(normalized, input, input_len + 1); - pcrypto_trim_ascii_inplace(normalized); + str_trim_ascii_inplace(normalized); if (normalized[0] == '\0') { return PM3_EINVARG; @@ -793,14 +720,14 @@ static int pcrypto_parse_ec_public_text(const char *input, bool allow_file_path, } // Only unescape after path resolution fails - pcrypto_unescape_newlines_inplace(normalized); + str_unescape_newlines_inplace(normalized); // Try as hex string uint8_t decoded[PCRYPTO_MAX_KEY_INPUT] = {0}; int decoded_len = -1; char compact[PCRYPTO_MAX_KEY_INPUT] = {0}; size_t compact_len = 0; - if (pcrypto_copy_without_whitespace(normalized, compact, sizeof(compact), &compact_len) == PM3_SUCCESS && + if (str_copy_without_whitespace(normalized, compact, sizeof(compact), &compact_len) == PM3_SUCCESS && compact_len > 0) { decoded_len = hex_to_bytes(compact, decoded, sizeof(decoded)); } diff --git a/client/src/fileutils.c b/client/src/fileutils.c index c5a3b8dfa..1101370e6 100644 --- a/client/src/fileutils.c +++ b/client/src/fileutils.c @@ -127,20 +127,176 @@ int fileExists(const char *filename) { * @param filename * @return */ -static bool is_directory(const char *filename) { +bool path_is_directory(const char *path) { + if (path == NULL) { + return false; + } #ifdef _WIN32 struct _stat st; - if (_stat(filename, &st) == -1) + if (_stat(path, &st) == -1) return false; #else struct stat st; // stat(filename, &st); - if (lstat(filename, &st) == -1) + if (lstat(path, &st) == -1) return false; #endif return S_ISDIR(st.st_mode) != 0; } +bool path_is_regular_file(const char *path) { + if (path == NULL) { + return false; + } +#ifdef _WIN32 + struct _stat st; + if (_stat(path, &st) == -1) + return false; +#else + struct stat st; + if (stat(path, &st) == -1) + return false; +#endif + return S_ISREG(st.st_mode) != 0; +} + +const char *path_basename(const char *path) { + if (path == NULL) { + return ""; + } + + const char *base = strrchr(path, '/'); + const char *base_win = strrchr(path, '\\'); + if (base == NULL || (base_win != NULL && base_win > base)) { + base = base_win; + } + return (base == NULL) ? path : (base + 1); +} + +void path_basename_without_ext(const char *path, char *out, size_t out_len) { + if (out == NULL || out_len == 0) { + return; + } + out[0] = '\0'; + + const char *base = path_basename(path); + if (base[0] == '\0') { + return; + } + + snprintf(out, out_len, "%s", base); + char *dot = strrchr(out, '.'); + if (dot != NULL && dot != out) { + *dot = '\0'; + } +} + +static int qsort_path_cmp(const void *a, const void *b) { + const char *pa = (const char *)a; + const char *pb = (const char *)b; + return strcmp(pa, pb); +} + +static char *path_list_slot(char *paths, size_t path_len, size_t index) { + return paths + (index * path_len); +} + +int collect_file_paths_recursive(const char *dirpath, char *paths, size_t path_len, + size_t max_paths, size_t *count, bool include_hidden, size_t max_depth) { + if (dirpath == NULL || paths == NULL || path_len == 0 || count == NULL) { + return PM3_EINVARG; + } + + DIR *dir = opendir(dirpath); + if (dir == NULL) { + return PM3_EFILE; + } + + struct dirent *entry = NULL; + while ((entry = readdir(dir)) != NULL) { + if (strcmp(entry->d_name, ".") == 0 || strcmp(entry->d_name, "..") == 0 || + (include_hidden == false && entry->d_name[0] == '.')) { + continue; + } + + char *fullpath = calloc(path_len, sizeof(char)); + if (fullpath == NULL) { + closedir(dir); + return PM3_EMALLOC; + } + const char *sep = ""; + size_t dir_len = strlen(dirpath); + if (dir_len > 0 && dirpath[dir_len - 1] != '/' && dirpath[dir_len - 1] != '\\') { + sep = PATHSEP; + } + if (snprintf(fullpath, path_len, "%s%s%s", dirpath, sep, entry->d_name) >= (int)path_len) { + free(fullpath); + continue; + } + + if (path_is_directory(fullpath)) { + if (max_depth == 0) { + free(fullpath); + continue; + } + int res = collect_file_paths_recursive(fullpath, paths, path_len, max_paths, count, include_hidden, max_depth - 1); + free(fullpath); + if (res != PM3_SUCCESS) { + closedir(dir); + return res; + } + continue; + } + + if (!path_is_regular_file(fullpath)) { + free(fullpath); + continue; + } + if (*count >= max_paths) { + free(fullpath); + closedir(dir); + return PM3_EOVFLOW; + } + if (snprintf(path_list_slot(paths, path_len, *count), path_len, "%s", fullpath) >= (int)path_len) { + free(fullpath); + continue; + } + free(fullpath); + (*count)++; + } + + closedir(dir); + return PM3_SUCCESS; +} + +int collect_resource_file_paths(const char *resource_dir, char *paths, size_t path_len, + size_t max_paths, size_t *count, bool include_hidden, size_t max_depth) { + if (resource_dir == NULL || resource_dir[0] == '\0' || paths == NULL || path_len == 0 || count == NULL) { + return PM3_EINVARG; + } + + char *rootdir = NULL; + int res = searchFile(&rootdir, RESOURCES_SUBDIR, resource_dir, "", true); + if (res != PM3_SUCCESS) { + return res; + } + + if (!path_is_directory(rootdir)) { + free(rootdir); + return PM3_EFILE; + } + + *count = 0; + res = collect_file_paths_recursive(rootdir, paths, path_len, max_paths, count, include_hidden, max_depth); + free(rootdir); + if (res != PM3_SUCCESS) { + return res; + } + + qsort(paths, *count, path_len, qsort_path_cmp); + return PM3_SUCCESS; +} + bool setDefaultPath(savePaths_t pathIndex, const char *path) { if (pathIndex < spItemCount) { @@ -2884,7 +3040,7 @@ static int filelist(const char *path, const char *ext, uint8_t last, bool tentat tmp_fullpath[1023] = 0x00; strncat(tmp_fullpath, namelist[i]->d_name, strlen(tmp_fullpath) - 1); - if (is_directory(tmp_fullpath)) { + if (path_is_directory(tmp_fullpath)) { char newpath[1024]; if (strcmp(namelist[i]->d_name, ".") == 0 || strcmp(namelist[i]->d_name, "..") == 0) @@ -3144,7 +3300,7 @@ int searchFile(char **foundpath, const char *pm3dir, const char *searchname, con return PM3_EINVARG; } - if (is_directory(searchname)) { + if (path_is_directory(searchname)) { return PM3_EINVARG; } @@ -3397,4 +3553,3 @@ int pm3_save_fm11rf08s_nonces(const char *fn, iso14a_fm11rf08s_nonces_with_data_ } return PM3_SUCCESS; } - diff --git a/client/src/fileutils.h b/client/src/fileutils.h index ef1642a3f..0642cf478 100644 --- a/client/src/fileutils.h +++ b/client/src/fileutils.h @@ -114,6 +114,42 @@ typedef enum { int fileExists(const char *filename); +/** + * @brief Check whether a path exists and is a directory. + */ +bool path_is_directory(const char *path); + +/** + * @brief Check whether a path exists and is a regular file. + */ +bool path_is_regular_file(const char *path); + +/** + * @brief Return the final path component, or an empty string for NULL input. + */ +const char *path_basename(const char *path); + +/** + * @brief Copy the final path component without its last file extension. + */ +void path_basename_without_ext(const char *path, char *out, size_t out_len); + +/** + * @brief Recursively collect regular file paths under a directory into fixed-size slots. + * + * A max_depth of 0 scans only dirpath and does not descend into subdirectories. + */ +int collect_file_paths_recursive(const char *dirpath, char *paths, size_t path_len, + size_t max_paths, size_t *count, bool include_hidden, size_t max_depth); + +/** + * @brief Resolve a resources subdirectory and recursively collect regular file paths from it. + * + * A max_depth of 0 scans only the resolved resource_dir and does not descend into subdirectories. + */ +int collect_resource_file_paths(const char *resource_dir, char *paths, size_t path_len, + size_t max_paths, size_t *count, bool include_hidden, size_t max_depth); + // set a path in the path list g_session.defaultPaths bool setDefaultPath(savePaths_t pathIndex, const char *path); diff --git a/client/src/util.c b/client/src/util.c index 68e9ec401..8f4b5589a 100644 --- a/client/src/util.c +++ b/client/src/util.c @@ -1944,3 +1944,75 @@ size_t unduplicate(uint8_t *d, size_t n, const uint8_t item_n) { return write_index; } + +void str_trim_ascii_inplace(char *s) { + if (s == NULL) { + return; + } + + size_t start = 0; + size_t len = strlen(s); + while (start < len && isspace((unsigned char)s[start])) { + start++; + } + while (len > start && isspace((unsigned char)s[len - 1])) { + len--; + } + + if (start > 0) { + memmove(s, s + start, len - start); + } + s[len - start] = '\0'; +} + +void str_unescape_newlines_inplace(char *s) { + if (s == NULL) { + return; + } + + size_t read_pos = 0; + size_t write_pos = 0; + size_t len = strlen(s); + while (read_pos < len) { + if (s[read_pos] == '\\' && (read_pos + 1) < len) { + char esc = s[read_pos + 1]; + if (esc == 'n') { + s[write_pos++] = '\n'; + read_pos += 2; + continue; + } + if (esc == 'r') { + s[write_pos++] = '\r'; + read_pos += 2; + continue; + } + if (esc == 't') { + s[write_pos++] = '\t'; + read_pos += 2; + continue; + } + } + s[write_pos++] = s[read_pos++]; + } + s[write_pos] = '\0'; +} + +int str_copy_without_whitespace(const char *src, char *dst, size_t dst_size, size_t *dst_len) { + if (src == NULL || dst == NULL || dst_len == NULL || dst_size == 0) { + return PM3_EINVARG; + } + + size_t out = 0; + for (size_t i = 0; src[i] != '\0'; i++) { + if (isspace((unsigned char)src[i])) { + continue; + } + if ((out + 1) >= dst_size) { + return PM3_EOVFLOW; + } + dst[out++] = src[i]; + } + dst[out] = '\0'; + *dst_len = out; + return PM3_SUCCESS; +} diff --git a/client/src/util.h b/client/src/util.h index 1ba9f8763..75cd7c9b3 100644 --- a/client/src/util.h +++ b/client/src/util.h @@ -215,4 +215,19 @@ uint8_t get_highest_frequency(const uint8_t *d, uint8_t n); size_t unduplicate(uint8_t *d, size_t n, const uint8_t item_n); +/** + * @brief Trim leading and trailing ASCII whitespace from a mutable string. + */ +void str_trim_ascii_inplace(char *s); + +/** + * @brief Replace escaped \n, \r, and \t sequences with their control characters in-place. + */ +void str_unescape_newlines_inplace(char *s); + +/** + * @brief Copy a string while dropping all ASCII whitespace characters. + */ +int str_copy_without_whitespace(const char *src, char *dst, size_t dst_size, size_t *dst_len); + #endif From 7abc9a15633fcec5123c40e4ef6bd8329db664fa Mon Sep 17 00:00:00 2001 From: kormax <3392860+kormax@users.noreply.github.com> Date: Tue, 5 May 2026 18:40:18 +0300 Subject: [PATCH 2/3] Use LEAF certificate instead of bare public key --- .../leaf_community-root-public-key.der | Bin 91 -> 0 bytes .../nxp-leaf-e200-sn63709320131000-c02.pem | 14 ++++++++++++ .../nxp-leaf-e500-sn63709320130000-c01.pem | 15 +++++++++++++ client/src/cmdhfmfdes.c | 20 ++++++++++++++---- 4 files changed, 45 insertions(+), 4 deletions(-) delete mode 100644 client/resources/duox_trust/leaf_community/leaf_community-root-public-key.der create mode 100644 client/resources/duox_trust/nxp/nxp-leaf-e200-sn63709320131000-c02.pem create mode 100644 client/resources/duox_trust/nxp/nxp-leaf-e500-sn63709320130000-c01.pem diff --git a/client/resources/duox_trust/leaf_community/leaf_community-root-public-key.der b/client/resources/duox_trust/leaf_community/leaf_community-root-public-key.der deleted file mode 100644 index e12db2a074d9d34a691629bc2eeed0f203979f0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 91 zcmXqrG!SNE*J|@PXUoLM#sOw9GqN)~F|g>WH|}#hq#m(MxRF2oO~rk|vLj{DirSuv tJvk8$T~W7eXBI2WZPJ)%)gd Date: Tue, 5 May 2026 18:48:03 +0300 Subject: [PATCH 3/3] Remove 'hf mfdes leaf'; use 'hf mfdes verifycert leaf' instead --- client/src/cmdhfmfdes.c | 235 ---------------------------------------- 1 file changed, 235 deletions(-) diff --git a/client/src/cmdhfmfdes.c b/client/src/cmdhfmfdes.c index 7b3f69e32..e9e9d3fbb 100644 --- a/client/src/cmdhfmfdes.c +++ b/client/src/cmdhfmfdes.c @@ -51,9 +51,6 @@ #include "mifare/prime.h" #include "util.h" #include "crypto/originality.h" -#include "x509_crt.h" -#include "mbedtls/oid.h" -#include "mbedtls/sha256.h" #define MAX_KEY_LEN 24 #define MAX_KEYS_LIST_LEN 1024 @@ -84,8 +81,6 @@ // LEAF Verified Open Application #define LEAF_VERIFIED_DEFAULT_AID 0xF51CD6U #define LEAF_VERIFIED_CERT_FILE 0x02 -#define LEAF_VERIFIED_MAX_CERT_LEN 4096 -#define LEAF_COMMUNITY_ROOT_CERT_PATH "duox_trust/nxp/nxp-leaf-e200-sn63709320131000-c02.pem" #define DUOX_VDE_DEFAULT_AID 0x1010F6U #define DUOX_VDE_CERT_FILE 0x00 @@ -8055,235 +8050,6 @@ static int CmdHF14ADesVdeSign(const char *Cmd) { } // Look up an attribute in a DN by OID. Returns pointer to mbedtls value buf or NULL. -static const mbedtls_x509_buf *leaf_dn_find_oid(const mbedtls_x509_name *dn, const char *oid_buf, size_t oid_len) { - while (dn != NULL) { - if (dn->oid.len == oid_len && memcmp(dn->oid.p, oid_buf, oid_len) == 0) - return &dn->val; - dn = dn->next; - } - return NULL; -} - -static int CmdHF14ADesLeaf(const char *Cmd) { - CLIParserContext *ctx; - CLIParserInit(&ctx, "hf mfdes leaf", - "Read and verify a LEAF Verified credential on a MIFARE DUOX card.\n" - "Selects the LEAF Verified Open Application, reads the X.509 certificate\n" - "from file 0x02, verifies it was signed by the LEAF Root CA, performs ISO\n" - "Internal Authenticate, and verifies the card signature with the public key\n" - "embedded in the certificate.", - "hf mfdes leaf -> verify with default AID D61CF5\n" - "hf mfdes leaf -v -> verbose output\n" - "hf mfdes leaf --aid D61CF5 -> override AID\n" - "hf mfdes leaf -d 00112233445566778899AABBCCDDEEFF -> explicit 16-byte challenge\n"); - - void *argtable[] = { - arg_param_begin, - arg_lit0("a", "apdu", "Show APDU requests and responses"), // 1 - arg_lit0("v", "verbose", "Verbose output"), // 2 - arg_str0("d", "challenge", "", "Challenge / RndA (16 bytes, random if omitted)"), // 3 - arg_str0(NULL, "aid", "", "Application ID (3 bytes, default D61CF5)"), // 4 - arg_int0("n", "keynum", "", "Key number (P2, default 0)"), // 5 - arg_str0(NULL, "isoid", "", "Application ISO ID / ISO DF FID (2 bytes)"), // 6 - arg_str0(NULL, "dfname", "", "Application ISO DF Name (1-16 hex bytes)"), // 7 - arg_param_end - }; - CLIExecWithReturn(ctx, Cmd, argtable, true); - - bool APDULogging = arg_get_lit(ctx, 1); - bool verbose = arg_get_lit(ctx, 2); - - uint8_t challenge[DUOX_INTAUTH_CHALLENGE_LEN] = {0}; - int challenge_len = 0; - CLIGetHexWithReturn(ctx, 3, challenge, &challenge_len); - bool challenge_provided = (challenge_len > 0); - if (challenge_provided && challenge_len != DUOX_INTAUTH_CHALLENGE_LEN) { - PrintAndLogEx(ERR, "Challenge must be exactly 16 bytes, got %d", challenge_len); - CLIParserFree(ctx); - return PM3_EINVARG; - } - - int keynum = arg_get_int_def(ctx, 5, 0); - if (keynum < 0 || keynum > 255) { - PrintAndLogEx(ERR, "Key number must be 0..255"); - CLIParserFree(ctx); - return PM3_EINVARG; - } - - mfd_app_select app_select = MfdSelectionInitAID(LEAF_VERIFIED_DEFAULT_AID); - if (MfdSelectionApplyCmdParameters(ctx, 4, 6, 7, &app_select) != PM3_SUCCESS) { - CLIParserFree(ctx); - return PM3_EINVARG; - } - - SetAPDULogging(APDULogging); - CLIParserFree(ctx); - - duox_certificate_anchor_t leaf_root_anchor = {0}; - int pk_res = duox_load_certificate_anchor_from_input(LEAF_COMMUNITY_ROOT_CERT_PATH, DUOX_DEFAULT_CA_DIR, &leaf_root_anchor); - mbedtls_ecp_group_id leaf_root_curveid = MBEDTLS_ECP_DP_NONE; - const uint8_t *leaf_root_anchor_pubkey = NULL; - size_t leaf_root_pubkey_len = 0; - if (pk_res == PM3_SUCCESS) { - pk_res = duox_certificate_anchor_public_key(&leaf_root_anchor, &leaf_root_curveid, &leaf_root_anchor_pubkey, &leaf_root_pubkey_len); - } - if (pk_res != PM3_SUCCESS) { - PrintAndLogEx(ERR, "Failed to load LEAF Root CA certificate from " _YELLOW_("%s") " (%d)", LEAF_COMMUNITY_ROOT_CERT_PATH, pk_res); - return pk_res; - } - if (leaf_root_curveid != MBEDTLS_ECP_DP_SECP256R1 || leaf_root_anchor_pubkey == NULL || leaf_root_pubkey_len != 65) { - PrintAndLogEx(ERR, "LEAF Root CA certificate has unsupported public key"); - return PM3_ECRYPTO; - } - uint8_t leaf_root_pubkey[65] = {0}; - memcpy(leaf_root_pubkey, leaf_root_anchor_pubkey, sizeof(leaf_root_pubkey)); - - if (!challenge_provided) { - int res = pcrypto_rng_fill_oneshot(challenge, sizeof(challenge), "hf_mfdes_leaf"); - if (res != PM3_SUCCESS) { - PrintAndLogEx(ERR, "Failed to generate random challenge"); - return res; - } - } - - PrintAndLogEx(INFO, "--- " _CYAN_("LEAF Verified Credential Check")); - if (verbose) { - MfdSelectionPrint(&app_select); - } - - // Step 1: Select application - DesfireContext_t dctx = {0}; - dctx.commMode = DCMPlain; - dctx.cmdSet = DCCNativeISO; - - if (MfdSelectionSelectApplication(&dctx, &app_select, verbose) != PM3_SUCCESS) { - DropField(); - return PM3_ESOFT; - } - - // Step 2: Read X.509 certificate from file 0x02 (length=0 reads to EOF) - uint8_t cert_buf[LEAF_VERIFIED_MAX_CERT_LEN] = {0}; - size_t cert_len = 0; - int res = DesfireReadFile(&dctx, LEAF_VERIFIED_CERT_FILE, 0, 0, cert_buf, &cert_len); - if (res != PM3_SUCCESS || cert_len == 0) { - PrintAndLogEx(ERR, "Read certificate file 0x%02X " _RED_("failed") " (%d)", LEAF_VERIFIED_CERT_FILE, res); - DropField(); - return PM3_ESOFT; - } - PrintAndLogEx(SUCCESS, "Certificate read " _GREEN_("ok") " (%zu bytes)", cert_len); - if (verbose) - print_hex_break(cert_buf, cert_len, 32); - - // Step 3: Parse certificate - mbedtls_x509_crt cert; - mbedtls_x509_crt_init(&cert); - int xres = mbedtls_x509_crt_parse_der(&cert, cert_buf, cert_len); - if (xres != 0) { - PrintAndLogEx(ERR, "X.509 parse " _RED_("failed") " (-0x%04x)", -xres); - mbedtls_x509_crt_free(&cert); - DropField(); - return PM3_ESOFT; - } - - // Print certificate details - PrintAndLogEx(INFO, "--- " _CYAN_("Certificate")); - - char dnbuf[256] = {0}; - mbedtls_x509_dn_gets(dnbuf, sizeof(dnbuf), &cert.subject); - PrintAndLogEx(INFO, "Subject...... " _YELLOW_("%s"), dnbuf); - mbedtls_x509_dn_gets(dnbuf, sizeof(dnbuf), &cert.issuer); - PrintAndLogEx(INFO, "Issuer....... " _YELLOW_("%s"), dnbuf); - - char idbuf[128] = {0}; - const mbedtls_x509_buf *open_id = leaf_dn_find_oid(&cert.subject, - MBEDTLS_OID_AT_SERIAL_NUMBER, - MBEDTLS_OID_SIZE(MBEDTLS_OID_AT_SERIAL_NUMBER)); - if (open_id != NULL && open_id->len > 0) { - size_t cp = (open_id->len < sizeof(idbuf) - 1) ? open_id->len : sizeof(idbuf) - 1; - memcpy(idbuf, open_id->p, cp); - PrintAndLogEx(INFO, "Open ID...... " _YELLOW_("%s"), idbuf); - } - - PrintAndLogEx(INFO, "Valid from... " _YELLOW_("%04d-%02d-%02d %02d:%02d:%02d"), - cert.valid_from.year, cert.valid_from.mon, cert.valid_from.day, - cert.valid_from.hour, cert.valid_from.min, cert.valid_from.sec); - PrintAndLogEx(INFO, "Valid to..... " _YELLOW_("%04d-%02d-%02d %02d:%02d:%02d"), - cert.valid_to.year, cert.valid_to.mon, cert.valid_to.day, - cert.valid_to.hour, cert.valid_to.min, cert.valid_to.sec); - - if (cert.serial.len > 0) - PrintAndLogEx(INFO, "Serial....... " _YELLOW_("%s"), sprint_hex_inrow(cert.serial.p, cert.serial.len)); - - uint8_t fp[32] = {0}; - if (mbedtls_sha256_ret(cert_buf, cert_len, fp, 0) == 0) - PrintAndLogEx(INFO, "SHA-256...... " _YELLOW_("%s"), sprint_hex_inrow(fp, sizeof(fp))); - - // Step 4: Verify certificate signature against LEAF Root CA public key. - // The certificate uses ECDSA-SHA256 over secp256r1; ecdsa_signature_verify - // accepts the DER-encoded signature stored in cert.sig. - PrintAndLogEx(INFO, "--- " _CYAN_("Root CA Verification")); - bool root_ok = false; - int rres = ecdsa_signature_verify( - MBEDTLS_ECP_DP_SECP256R1, - leaf_root_pubkey, - cert.tbs.p, - (int)cert.tbs.len, - cert.sig.p, - cert.sig.len, - true); - if (rres == PM3_SUCCESS) { - PrintAndLogEx(SUCCESS, "Root signature " _GREEN_("verified") " (LEAF Root CA P-256)"); - root_ok = true; - } else { - PrintAndLogEx(ERR, "Root signature " _RED_("verification failed") " (%d)", rres); - } - - // Extract card public key (P-256, uncompressed) - uint8_t card_pubkey[65] = {0}; - int kres = ecdsa_public_key_from_pk(&cert.pk, MBEDTLS_ECP_DP_SECP256R1, card_pubkey, sizeof(card_pubkey)); - mbedtls_x509_crt_free(&cert); - if (kres != 0) { - PrintAndLogEx(ERR, "Failed to extract card public key (-0x%04x)", -kres); - DropField(); - return PM3_ESOFT; - } - if (verbose) - PrintAndLogEx(INFO, "Card pubkey.. %s", sprint_hex_inrow(card_pubkey, sizeof(card_pubkey))); - - // Step 5: ISO Internal Authenticate - PrintAndLogEx(INFO, "--- " _CYAN_("ISO Internal Authenticate")); - PrintAndLogEx(INFO, "Challenge.... " _YELLOW_("%s"), sprint_hex_inrow(challenge, sizeof(challenge))); - uint8_t card_random[DUOX_INTAUTH_CHALLENGE_LEN] = {0}; - uint8_t signature_rs[DUOX_INTAUTH_SIG_LEN] = {0}; - res = duox_intauth_exchange(APDULogging, verbose, (uint8_t)keynum, challenge, card_random, signature_rs); - DropField(); - if (res != PM3_SUCCESS) - return res; - - // Step 6: Verify card signature with extracted public key. - PrintAndLogEx(INFO, "--- " _CYAN_("Signature Verification")); - bool card_ok = false; - int sig_res = duox_intauth_verify_sig(verbose, card_pubkey, challenge, card_random, signature_rs); - if (sig_res == PM3_SUCCESS) { - PrintAndLogEx(SUCCESS, "Card signature " _GREEN_("verified")); - card_ok = true; - } else { - PrintAndLogEx(ERR, "Card signature " _RED_("verification failed")); - } - - PrintAndLogEx(NORMAL, ""); - if (root_ok && card_ok) { - PrintAndLogEx(SUCCESS, "LEAF Verified credential " _GREEN_("AUTHENTIC")); - if (idbuf[0] != '\0') - PrintAndLogEx(SUCCESS, "Open ID...... " _GREEN_("%s"), idbuf); - } else { - PrintAndLogEx(ERR, "LEAF Verified credential " _RED_("FAILED") " (root=%s, card=%s)", - root_ok ? "ok" : "fail", card_ok ? "ok" : "fail"); - } - - return (root_ok && card_ok) ? PM3_SUCCESS : PM3_ESOFT; -} - static const CLIParserOption mfdesValidateMethodOpts[] = { {MFDES_VALIDATE_METHOD_AUTO, "auto"}, {MFDES_VALIDATE_METHOD_INTAUTH, "intauth"}, @@ -9341,7 +9107,6 @@ static command_t CommandTable[] = { {"verifycert", CmdHF14ADesVerifyCert, IfPm3Iso14443a, "Validate cert from file and verify key possession"}, {"intauth", CmdHF14ADesIntAuth, IfPm3Iso14443a, "ISO Internal Authenticate (ECDSA challenge-response)"}, {"vdesign", CmdHF14ADesVdeSign, IfPm3Iso14443a, "VDE ECDSASign (EV charging signature over 32-byte challenge)"}, - {"leaf", CmdHF14ADesLeaf, IfPm3Iso14443a, "LEAF Verified credential read + cert + auth check"}, {"-----------", CmdHelp, IfPm3Iso14443a, "----------------------- " _CYAN_("System") " -----------------------"}, {"test", CmdHF14ADesTest, AlwaysAvailable, "Regression crypto tests"}, {NULL, NULL, NULL, NULL}