otp: created secret display for otp
What changed, and why it matters
This commit adds a new on-screen feature that lets a user view the raw secret key for an OTP (one-time password) account they already have stored on their Jade hardware wallet. It is a user-interface change, not a remote attack. The main security consideration is that it makes it easier for someone with physical access to the unlocked device to read or photograph an OTP secret, whereas before only a QR-encoded URI could be displayed. The change also fixes a small UI inconsistency by using the standard back-button event ID.
Treat as a normal feature commit. Review whether the new plain-text secret display aligns with the product's threat model for physical-access and shoulder-surfing scenarios, and ensure the secret buffer is cleared promptly. No immediate security patch is indicated by the diff alone.
Security signals we found
New UI path exposes raw OTP secret in plain text on device screen
Secret is displayed using a local stack buffer marked with SENSITIVE_PUSH/SENSITIVE_POP
Function signature change widens data passed to QR activity from name-only to full OTP context
No input validation changes or cryptographic changes present
No vendor statement of security relevance in commit or supplied references
Evidence from the diff
The patch introduces show_otp_secret_text_activity() in main/qrmode.c, which formats the OTP secret with split_text() and displays it in a text-grid activity. It wires this into the existing OTP QR display so that pressing BTN_OTP_DETAILS_SECRET shows the secret text. The function signature of show_otp_uri_qr_activity() is changed from taking a name string to taking the full otpauth_ctx_t*, allowing the secret bytes to be passed through. SENSITIVE_PUSH/SENSITIVE_POP are used around the local display buffer. A header button event ID is corrected from BTN_XPUB_EXIT to BTN_BACK.
Changed components
main/process/dashboard.cmain/qrmode.cmain/qrmode.hmain/ui/qrmode.cInspect captured patch +53 / −8
diff --git a/main/process/dashboard.c b/main/process/dashboard.c
index c085190..a503657 100644
--- a/main/process/dashboard.c
+++ b/main/process/dashboard.c
@@ -1452,7 +1452,7 @@ static bool show_otp_detail_options_activity(
} else if (ev_id == BTN_OTP_DETAILS_VIEW) {
show_otp_details_activity(otp_ctx, initial_confirmation, is_valid, show_delete_btn);
} else if (ev_id == BTN_OTP_DETAILS_EXPORT) {
- show_otp_uri_qr_activity(otp_ctx->name);
+ show_otp_uri_qr_activity(otp_ctx);
}
}
}
diff --git a/main/qrmode.c b/main/qrmode.c
index 6ba4d1a..06a64e9 100644
--- a/main/qrmode.c
+++ b/main/qrmode.c
@@ -20,6 +20,7 @@
#include "utils/address.h"
#include "utils/malloc_ext.h"
#include "utils/network.h"
+#include "utils/util.h"
#include "wallet.h"
#include <wally_script.h>
@@ -36,6 +37,12 @@
#define ACCOUNT_INDEX_MAX 65536
#define ACCOUNT_INDEX_FLAGS_SHIFT 16
+#define MAX_OTP_SCREENS 1
+#define OTP_TEXTSPLITLEN 4
+#define OTP_GRID_TOPPAD 4
+#define OTP_GRID_X 4
+#define OTP_GRID_Y 6
+#define OTP_GRID_SIZE (OTP_GRID_X * OTP_GRID_Y)
// When we are displaying a BCUR QR code we ensure the timeout is at least this value
// as we don't want the unit to shut down because of apparent inactivity.
#define BCUR_QR_DISPLAY_MIN_TIMEOUT_SECS 300
@@ -1443,9 +1450,44 @@ static void add_cr_after_last_slash(const char* url, char* output, const size_t
strcpy(output + index + 2, url + index + 1);
}
-bool show_otp_uri_qr_activity(const char* otp_name)
+bool show_otp_secret_text_activity(const otpauth_ctx_t* otp_ctx)
{
- JADE_ASSERT(otp_name);
+ JADE_ASSERT(otp_ctx);
+
+ size_t num_words = 0;
+ size_t words_len = 0;
+ char secret_display[256];
+ SENSITIVE_PUSH(secret_display, sizeof(secret_display));
+
+ split_text(otp_ctx->secret, otp_ctx->secret_len, OTP_TEXTSPLITLEN, secret_display, sizeof(secret_display),
+ &num_words, &words_len);
+ JADE_ASSERT(num_words <= MAX_OTP_SCREENS * OTP_GRID_SIZE);
+ JADE_ASSERT(words_len <= sizeof(secret_display));
+
+ btn_data_t hdrbtns[] = { { .txt = "=", .font = JADE_SYMBOLS_16x16_FONT, .ev_id = BTN_BACK },
+ { .txt = NULL, .font = GUI_DEFAULT_FONT, .ev_id = GUI_BUTTON_EVENT_NONE } };
+
+ const char* remaining_words = NULL;
+ gui_activity_t* const act = make_text_grid_activity("Secret Key", hdrbtns, 2, OTP_GRID_TOPPAD, OTP_GRID_X,
+ OTP_GRID_Y, secret_display, num_words, GUI_DEFAULT_FONT, &remaining_words);
+ JADE_ASSERT(remaining_words == secret_display + words_len);
+ gui_set_current_activity(act);
+
+ int32_t ev_id;
+ while (gui_activity_wait_event(act, GUI_BUTTON_EVENT, ESP_EVENT_ANY_ID, NULL, &ev_id, NULL, 0)) {
+ if (ev_id == BTN_BACK) {
+ break;
+ }
+ }
+
+ SENSITIVE_POP(secret_display);
+ return true;
+}
+
+bool show_otp_uri_qr_activity(const otpauth_ctx_t* otp_ctx)
+{
+ JADE_ASSERT(otp_ctx);
+ JADE_ASSERT(otp_ctx->name);
bool ok = false;
char uri[OTP_MAX_URI_LEN];
@@ -1454,7 +1496,7 @@ bool show_otp_uri_qr_activity(const char* otp_name)
SENSITIVE_PUSH(uri, sizeof(uri));
- if (!otp_load_uri(otp_name, uri, sizeof(uri), &written) || !written) {
+ if (!otp_load_uri(otp_ctx->name, uri, sizeof(uri), &written) || !written) {
const char* msg[] = { "Failed to load", "OTP URI" };
await_error_activity(msg, 2);
goto cleanup;
@@ -1471,7 +1513,7 @@ bool show_otp_uri_qr_activity(const char* otp_name)
JADE_ASSERT(qr_icon->data && qr_icon->width && qr_icon->height);
SENSITIVE_PUSH(qr_icon->data, qrcode_get_icon_data_size(qr_icon->width, qr_icon->height));
- gui_activity_t* const act = make_show_otp_qr_actvity(otp_name, qr_icon);
+ gui_activity_t* const act = make_show_otp_qr_actvity(otp_ctx->name, qr_icon);
int32_t ev_id;
while (true) {
@@ -1480,6 +1522,8 @@ bool show_otp_uri_qr_activity(const char* otp_name)
if (ev_id == BTN_BACK) {
ok = true;
goto cleanup;
+ } else if (ev_id == BTN_OTP_DETAILS_SECRET) {
+ show_otp_secret_text_activity(otp_ctx);
} else if (ev_id == BTN_QR_BRIGHTNESS) {
gui_next_qrcode_color();
gui_repaint(act->root_node);
diff --git a/main/qrmode.h b/main/qrmode.h
index 5ca2a0d..5ba15db 100644
--- a/main/qrmode.h
+++ b/main/qrmode.h
@@ -28,8 +28,8 @@ void await_qr_help_activity(const char* url);
bool await_qr_back_continue_activity(
const char* message[], size_t message_size, const char* url, bool default_selection);
-// Display a QR code for the OTP URI of the given otp name
-bool show_otp_uri_qr_activity(const char* otp_name);
+// Display a QR code for the OTP context
+bool show_otp_uri_qr_activity(const otpauth_ctx_t* otp_ctx);
// Start pinserver authentication via qr codes
void handle_qr_auth(bool suppress_pin_change_confirmation);
diff --git a/main/ui/qrmode.c b/main/ui/qrmode.c
index 07627c0..fc54678 100644
--- a/main/ui/qrmode.c
+++ b/main/ui/qrmode.c
@@ -226,6 +226,7 @@ gui_activity_t* make_qr_options_activity(gui_view_node_t** density_textbox, gui_
return make_menu_activity("QR Settings", hdrbtns, 2, menubtns, 2);
}
+// NOTE: 'icons' passed in here must be heap-allocated as the gui element takes ownership
gui_activity_t* make_show_otp_qr_actvity(const char* otp_name, Icon* qr_icon)
{
@@ -246,7 +247,7 @@ gui_activity_t* make_show_otp_qr_actvity(const char* otp_name, Icon* qr_icon)
// back button
btn_data_t hdrbtns[]
- = { { .txt = "=", .font = JADE_SYMBOLS_16x16_FONT, .ev_id = BTN_XPUB_EXIT, .borders = GUI_BORDER_ALL },
+ = { { .txt = "=", .font = JADE_SYMBOLS_16x16_FONT, .ev_id = BTN_BACK, .borders = GUI_BORDER_ALL },
{ .txt = NULL, .font = GUI_DEFAULT_FONT, .ev_id = GUI_BUTTON_EVENT_NONE },
{ .txt = "P", .font = JADE_SYMBOLS_16x16_FONT, .ev_id = BTN_QR_BRIGHTNESS, .borders = GUI_BORDER_ALL } };
add_buttons(vsplit, UI_ROW, hdrbtns, 3); // 44 (hsplit) / 3 == 14 - almost 15 so ok
Why this scored 17/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.