utils: add bin_to_base32, move base32_to_bin and add support for padding
What changed, and why it matters
This commit refactors how Blockstream Jade handles Base32-encoded one-time-password (OTP) secrets. It moves the Base32 decoder into a shared utility, adds a new encoder, and makes padding characters ('=') stop decoding instead of being rejected. The change is a routine code cleanup; there is no direct evidence in the commit that it fixes an active security vulnerability.
Treat as a normal refactor. If the Base32 decoder is used elsewhere, verify that accepting '=' as terminator does not allow truncated secrets to be processed silently. No urgent security action is indicated by this commit alone.
Security signals we found
Base32 decoder behavior changed: '=' padding now terminates decode loop instead of causing failure
mbedtls_md_context_t initialization moved into helper function, slightly tightening lifecycle
No mention of CVE, security fix, vulnerability, or bug disclosure in commit message or diff
No bounds-check regression observed; output buffer checks remain present
Evidence from the diff
The patch relocates base32_to_bin() from main/otpauth.c to main/utils/util.c and adds a new bin_to_base32(). The decoder now treats ‘=’ padding as an end-of-input terminator rather than an invalid character. otpauth.c is updated to call the shared decoder and to initialize the mbedtls_md_context_t inside prepare_md_ctx() instead of the caller. These are structural improvements; the commit message and diff do not describe a security bug or credit a security reporter.
Changed components
main/otpauth.cmain/utils/util.cmain/utils/util.hInspect captured patch +94 / −53
diff --git a/main/otpauth.c b/main/otpauth.c
index 9a76e36..f995407 100644
--- a/main/otpauth.c
+++ b/main/otpauth.c
@@ -261,47 +261,6 @@ static inline mbedtls_md_type_t get_md_type(const otpauth_ctx_t* otp_ctx)
}
}
-static bool base32_to_bin(
- const char* b32_str, const size_t b32_str_len, uint8_t* b32_dec, const size_t b32_dec_len, size_t* done)
-{
- JADE_ASSERT(b32_str);
- JADE_ASSERT(b32_str_len);
- JADE_ASSERT(b32_dec);
- JADE_ASSERT(b32_dec_len);
- JADE_ASSERT(done);
-
- unsigned int tmp = 0;
- uint8_t count = 0;
- *done = 0;
- const char* b32_str_end = b32_str + b32_str_len;
- while (b32_str < b32_str_end && *b32_str) {
- char ch = *b32_str++;
-
- if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
- ch = (ch & 0x1F) - 1;
- } else if (ch >= '2' && ch <= '7') {
- ch -= 24;
- } else {
- // Bad character
- return false;
- }
-
- tmp <<= 5;
- tmp |= ch;
- count += 5;
- if (count >= 8) {
- if (*done < b32_dec_len) {
- b32_dec[(*done)++] = tmp >> (count - 8);
- count -= 8;
- } else {
- // Destination size insufficient
- return false;
- }
- }
- }
- return true;
-}
-
static void pad_secret(uint8_t* secret, size_t* secret_len, const size_t min_size)
{
JADE_ASSERT(secret);
@@ -335,12 +294,9 @@ static bool prepare_md_ctx(const otpauth_ctx_t* otp_ctx, mbedtls_md_context_t* m
JADE_ASSERT(otp_is_valid(otp_ctx));
JADE_ASSERT(md_ctx);
+ mbedtls_md_init(md_ctx);
mbedtls_md_type_t md_type = get_md_type(otp_ctx);
OTP_CHECK_BOOL_RETURN(mbedtls_md_setup(md_ctx, mbedtls_md_info_from_type(md_type), 1) == 0);
- // FIXME: use getters instead of MBEDTLS_PRIVATE MACRO
- const size_t hmac_size = mbedtls_md_get_size(md_ctx->MBEDTLS_PRIVATE(md_info));
-
- const char* ptr = otp_ctx->secret;
// Sanity check - can't really happen atm as entire URI length is limited
if (otp_ctx->secret_len / 1.6 > SECRET_BUFSIZE) {
@@ -348,11 +304,9 @@ static bool prepare_md_ctx(const otpauth_ctx_t* otp_ctx, mbedtls_md_context_t* m
return false;
}
- size_t done = 0;
- uint8_t b32_dec[SECRET_BUFSIZE];
- const bool base32_decode_result = base32_to_bin(ptr, otp_ctx->secret_len, b32_dec, sizeof(b32_dec), &done);
-
- if (!base32_decode_result || !done) {
+ uint8_t secret_bin[SECRET_BUFSIZE];
+ size_t secret_bin_len = base32_to_bin(otp_ctx->secret, otp_ctx->secret_len, secret_bin, sizeof(secret_bin));
+ if (!secret_bin_len) {
JADE_LOGE("Bad Base32 secret decode - secret: %.*s", otp_ctx->secret_len, otp_ctx->secret);
return false;
}
@@ -362,10 +316,12 @@ static bool prepare_md_ctx(const otpauth_ctx_t* otp_ctx, mbedtls_md_context_t* m
// as appears necessary to match the test vectors in rfc6238.
// (See also https://github.com/Daegalus/dart-otp#global-settings)
if (md_type != MBEDTLS_MD_SHA1) {
- pad_secret(b32_dec, &done, hmac_size);
+ // FIXME: use getters instead of MBEDTLS_PRIVATE MACRO
+ const size_t hmac_size = mbedtls_md_get_size(md_ctx->MBEDTLS_PRIVATE(md_info));
+ pad_secret(secret_bin, &secret_bin_len, hmac_size);
}
- return mbedtls_md_hmac_starts(md_ctx, b32_dec, done) == 0;
+ return mbedtls_md_hmac_starts(md_ctx, secret_bin, secret_bin_len) == 0;
}
bool otp_get_auth_code(const otpauth_ctx_t* otp_ctx, char* token, const size_t token_len)
@@ -377,7 +333,6 @@ bool otp_get_auth_code(const otpauth_ctx_t* otp_ctx, char* token, const size_t t
// Calculate otp hmac of counter (be) with the secret as the key
mbedtls_md_context_t md_ctx;
- mbedtls_md_init(&md_ctx);
OTP_CHECK_BOOL_RETURN(prepare_md_ctx(otp_ctx, &md_ctx));
const size_t hmac_last_index = mbedtls_md_get_size(md_ctx.MBEDTLS_PRIVATE(md_info)) - 1;
diff --git a/main/utils/util.c b/main/utils/util.c
index 015d97d..fe5218d 100644
--- a/main/utils/util.c
+++ b/main/utils/util.c
@@ -94,4 +94,85 @@ void split_text(const char* src, const size_t len, const size_t wordlen, char* o
JADE_ASSERT(write <= output_len);
*written = write;
}
+
+size_t base32_to_bin(const char* b32_str, const size_t b32_str_len, uint8_t* bin, const size_t bin_len)
+{
+ JADE_ASSERT(b32_str && b32_str_len);
+ JADE_ASSERT(bin && bin_len);
+
+ size_t written = 0;
+ unsigned int tmp = 0;
+ uint8_t num_bits = 0;
+ const char* b32_str_end = b32_str + b32_str_len;
+ while (b32_str < b32_str_end && *b32_str) {
+ char ch = *b32_str++;
+
+ if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z')) {
+ ch = (ch & 0x1F) - 1;
+ } else if (ch >= '2' && ch <= '7') {
+ ch -= 24;
+ } else if (ch == '=') {
+ break; // Padding char - end of meaningful input
+ } else {
+ return 0; // Bad character
+ }
+
+ tmp <<= 5;
+ tmp |= ch;
+ num_bits += 5; // Read 5 bits
+ if (num_bits >= 8) {
+ // Write 8 bits
+ if (written >= bin_len) {
+ return 0; // Destination size insufficient
+ }
+ num_bits -= 8;
+ bin[written++] = tmp >> num_bits;
+ }
+ }
+ return written;
+}
+
+static const char b32_alphabet[32] = {
+ // Base 32 encoding characters from rfc4648
+ 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W',
+ 'X', 'Y', 'Z', '2', '3', '4', '5', '6', '7'
+};
+
+bool bin_to_base32(const uint8_t* bin, const size_t bin_len, char* b32_str, const size_t b32_str_len, bool use_padding)
+{
+ JADE_ASSERT(bin && bin_len);
+ JADE_ASSERT(b32_str && b32_str_len);
+
+ const size_t required_chars = (bin_len * 8 + 4) / 5;
+ size_t required_padding = use_padding && required_chars % 8 ? 8 - required_chars % 8 : 0;
+
+ if (b32_str_len < required_chars + required_padding + 1) {
+ JADE_LOGE("Buffer too small in bin_to_base32");
+ return false; // Destination size insufficient
+ }
+
+ unsigned int tmp = 0;
+ uint8_t num_bits = 0;
+ const uint8_t* bin_end = bin + bin_len;
+ char* out = b32_str;
+ while (bin < bin_end) {
+ tmp <<= 8;
+ tmp |= *bin++;
+ num_bits += 8;
+ while (num_bits >= 5) {
+ *out++ = b32_alphabet[(tmp >> (num_bits - 5)) & 0x1F];
+ num_bits -= 5;
+ }
+ }
+ if (num_bits > 0) {
+ *out++ = b32_alphabet[(tmp << (5 - num_bits)) & 0x1F];
+ }
+ // Append '=' padding to a multiple of 8 if requested
+ while (required_padding) {
+ *out++ = '=';
+ --required_padding;
+ }
+ *out = '\0';
+ return true;
+}
#endif // AMALGAMATED_BUILD
diff --git a/main/utils/util.h b/main/utils/util.h
index e4aabca..4028224 100644
--- a/main/utils/util.h
+++ b/main/utils/util.h
@@ -120,4 +120,9 @@ bool is_potential_green_user_path(const uint32_t* path, size_t path_len, uint32_
bool is_potential_green_recovery_path(const uint32_t* path, size_t path_len);
bool is_potential_green_server_path(const uint32_t* path, size_t path_len, uint32_t* subaccount_out);
+// Helper function to convert a base32 string to binary, returns 0 on failure
+size_t base32_to_bin(const char* b32_str, size_t b32_str_len, uint8_t* bin, size_t bin_len);
+// Helper function to convert binary data to a base32 string, padding optional
+bool bin_to_base32(const uint8_t* bin, size_t bin_len, char* b32_str, size_t b32_str_len, bool use_padding);
+
#endif /* UTIL_H_ */
Why this scored 21/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.