rpc: add show_bip85_bip39_entropy RPC command
What changed, and why it matters
This commit adds a new device command that lets a connected computer or phone ask a Blockstream Jade hardware wallet to display an encrypted BIP85 seed-entropy backup as a QR code. The change also tightens validation of the BIP32 index in several places, fixing a place where an out-of-range index was accepted. There is no direct evidence in the diff of a vulnerability being exploited, but the new QR-display path and the tightened validation are security-relevant.
Treat this as a normal feature/security-hardening commit. Reviewers should verify that the new `show_bip85_bip39_entropy_process` requires the same user confirmation as `get_bip85_bip39_entropy`, that the QR payload is encrypted to the host pubkey before display, and that the new `BIP32_MAX_CHILD_INDEX` checks cover all call sites. No immediate incident response is indicated by the diff alone.
Security signals we found
New RPC exposes encrypted BIP85 entropy via on-device QR display
BIP32 index bounds checks added in multiple RPC parameter parsers
Sensitive stack buffers now marked with SENSITIVE_PUSH/POP in QR reply path
Help URL changed from blkstrm.com/bip85 to blkstrm.com/swaps85 for swap-specific flow
Existing user-confirmation dialog reused before entropy is exported
Evidence from the diff
The patch introduces show_bip85_bip39_entropy RPC/process, which derives BIP85-BIP39 entropy, encrypts it to a host-supplied ephemeral public key, and displays the result as a BC-UR QR code. It reuses the existing get_bip85_bip39_entropy_cbor helper, so user confirmation and AES encryption are preserved. The patch also adds BIP32_MAX_CHILD_INDEX (0x7fffffff) checks in get_bip85_bip39_entropy_data, get_bip85_rsa_entropy_data, params_identity_curve_index, and params_get_bip85_rsa_key, closing a parameter-validation gap. Minor UI string/help-URL changes are included for the cross-chain swap flow.
Changed components
jadepy/jade.py (Python API client)main/process/dashboard.c (RPC dispatch)main/process/get_bip85_entropy.c (BIP85 entropy derivation and new process)main/process/process_utils.c (parameter validation helpers)main/qrmode.c (QR display logic)main/qrmode.h (QR display header)main/utils/util.h (BIP32 constants)Inspect captured patch +115 / −27
diff --git a/jadepy/jade.py b/jadepy/jade.py
index 78691f1..eb29b22 100644
--- a/jadepy/jade.py
+++ b/jadepy/jade.py
@@ -758,6 +758,32 @@ class JadeAPI:
'pubkey': pubkey}
return self._jadeRpc('get_bip85_bip39_entropy', params)
+ def show_bip85_bip39_entropy(self, num_words, index, pubkey):
+ """
+ RPC call to show bip85-bip39 entropy as a qr code.
+
+ Parameters
+ ----------
+ num_words : int
+ The number of words the entropy is required to produce.
+
+ index : int
+ The index to use in the bip32 path to calculate the entropy.
+
+ pubkey: 33-bytes
+ The host ephemeral pubkey to use to generate a shared ecdh secret to use as an AES key
+ to encrypt the returned entropy.
+
+ Returns
+ -------
+ bool
+ True on success.
+ """
+ params = {'num_words': num_words,
+ 'index': index,
+ 'pubkey': pubkey}
+ return self._jadeRpc('show_bip85_bip39_entropy', params)
+
def get_bip85_rsa_entropy(self, key_bits, index, pubkey):
"""
RPC call to fetch encrypted bip85-rsa entropy.
diff --git a/main/process/dashboard.c b/main/process/dashboard.c
index 4003085..c52df11 100644
--- a/main/process/dashboard.c
+++ b/main/process/dashboard.c
@@ -145,6 +145,7 @@ void get_blinding_factor_process(void* process_ptr);
void sign_liquid_tx_process(void* process_ptr);
void get_bip85_pubkey_process(void* process_ptr);
void sign_bip85_digests_process(void* process_ptr);
+void show_bip85_bip39_entropy_process(void* process_ptr);
#ifdef CONFIG_DEBUG_MODE
void get_bip85_bip39_entropy_process(void* process_ptr);
void get_bip85_rsa_entropy_process(void* process_ptr);
@@ -599,6 +600,8 @@ static void dispatch_message(jade_process_t* process)
task_function = get_bip85_pubkey_process;
} else if (IS_METHOD("sign_bip85_digests")) {
task_function = sign_bip85_digests_process;
+ } else if (IS_METHOD("show_bip85_bip39_entropy")) {
+ task_function = show_bip85_bip39_entropy_process;
} else if (IS_METHOD("ota_data") || IS_METHOD("ota_complete") || IS_METHOD("tx_input")
|| IS_METHOD("get_extended_data") || IS_METHOD("get_signature") || IS_METHOD("pin")) {
// Method we only expect as part of a multi-message protocol
diff --git a/main/process/get_bip85_entropy.c b/main/process/get_bip85_entropy.c
index b0488e1..3c36c98 100644
--- a/main/process/get_bip85_entropy.c
+++ b/main/process/get_bip85_entropy.c
@@ -1,13 +1,16 @@
#ifndef AMALGAMATED_BUILD
#include "../aes.h"
+#include "../bcur.h"
#include "../button_events.h"
#include "../jade_assert.h"
#include "../jade_wally_verify.h"
#include "../process.h"
+#include "../qrmode.h"
#include "../random.h"
#include "../rsa.h"
#include "../sensitive.h"
#include "../ui.h"
+#include "../utils/util.h"
#include "../wallet.h"
#include "process_utils.h"
@@ -23,8 +26,6 @@ typedef struct {
size_t encrypted_len;
} bip85_data_t;
-void await_qr_help_activity(const char* url);
-
static void populate_bip85_reply_data(CborEncoder* container, const bip85_data_t* bip85_data)
{
JADE_ASSERT(container);
@@ -170,7 +171,7 @@ static int get_bip85_bip39_entropy_data(const CborValue* params, bip85_data_t* b
}
size_t index = 0;
- if (!rpc_get_sizet("index", params, &index)) {
+ if (!rpc_get_sizet("index", params, &index) || index > BIP32_MAX_CHILD_INDEX) {
*errmsg = "Failed to fetch valid index from message";
return CBOR_RPC_BAD_PARAMETERS;
}
@@ -186,12 +187,10 @@ static int get_bip85_bip39_entropy_data(const CborValue* params, bip85_data_t* b
// Special case for cross-chain swaps
if (nwords == 12 && index == 26589) {
// User to confirm
- const char* message[] = { "Scan successful!", "Continue to pair", "with wallet app." };
+ const char* message[] = { "Continue to pair", "with wallet app." };
- if (!await_continueback_activity("Enable Swaps", message, 3, false, "blkstrm.com/bip85")) {
- // User declined
- *errmsg = "User declined to export entropy";
- return CBOR_RPC_USER_CANCELLED;
+ if (!await_continueback_activity("Enable Swaps", message, 2, false, "blkstrm.com/swaps85")) {
+ goto user_cancel;
}
} else {
// User to confirm
@@ -199,14 +198,14 @@ static int get_bip85_bip39_entropy_data(const CborValue* params, bip85_data_t* b
int ret = snprintf(nwordphrase, sizeof(nwordphrase), "%u word seed phrase", nwords);
JADE_ASSERT(ret > 0 && ret < sizeof(nwordphrase));
- char txtindex[24];
+ char txtindex[32];
ret = snprintf(txtindex, sizeof(txtindex), "for BIP85 index %u?", index);
JADE_ASSERT(ret > 0 && ret < sizeof(txtindex));
const char* message[] = { "Export an encrypted", nwordphrase, txtindex };
if (!await_continueback_activity("Key Export", message, 3, false, "blkstrm.com/bip85")) {
- // User declined
+ user_cancel:
*errmsg = "User declined to export entropy";
return CBOR_RPC_USER_CANCELLED;
}
@@ -236,7 +235,7 @@ static int get_bip85_rsa_entropy_data(const CborValue* params, bip85_data_t* bip
}
size_t index = 0;
- if (!rpc_get_sizet("index", params, &index)) {
+ if (!rpc_get_sizet("index", params, &index) || index > BIP32_MAX_CHILD_INDEX) {
*errmsg = "Failed to fetch valid index from message";
return CBOR_RPC_BAD_PARAMETERS;
}
@@ -254,7 +253,7 @@ static int get_bip85_rsa_entropy_data(const CborValue* params, bip85_data_t* bip
int ret = snprintf(nwordphrase, sizeof(nwordphrase), "%u rsa key size", key_bits);
JADE_ASSERT(ret > 0 && ret < sizeof(nwordphrase));
- char txtindex[24];
+ char txtindex[32];
ret = snprintf(txtindex, sizeof(txtindex), "for BIP85 index %u?", index);
JADE_ASSERT(ret > 0 && ret < sizeof(txtindex));
@@ -348,4 +347,48 @@ void get_bip85_rsa_entropy_process(void* process_ptr)
cleanup:
return;
}
+
+void show_bip85_bip39_entropy_process(void* process_ptr)
+{
+ JADE_LOGI("Starting: %d", xPortGetFreeHeapSize());
+ jade_process_t* process = process_ptr;
+
+ // We expect a current message to be present
+ ASSERT_CURRENT_MESSAGE(process, "show_bip85_bip39_entropy");
+ ASSERT_KEYCHAIN_UNLOCKED_BY_MESSAGE_SOURCE(process);
+ GET_MSG_PARAMS(process);
+
+ const char* errmsg = NULL;
+ uint8_t cbor[176]; // sufficient for encrypted bip85 reply
+ size_t cbor_len;
+
+ SENSITIVE_PUSH(cbor, sizeof(cbor));
+
+ CborEncoder reply_encoder;
+ cbor_encoder_init(&reply_encoder, cbor, sizeof(cbor), 0);
+
+ const int errcode = get_bip85_bip39_entropy_cbor(¶ms, &reply_encoder, &errmsg);
+ if (errcode) {
+ if (errcode != CBOR_RPC_USER_CANCELLED) {
+ JADE_LOGE("Error generating encrypted bip85 entropy: %s", errmsg);
+ const char* message[] = { "Error in bip85/bip39", errmsg };
+ await_error_activity(message, 2);
+ }
+ // An error occurred, or the user cancelled the action
+ jade_process_reject_message(process, errcode, errmsg);
+ goto cleanup;
+ }
+
+ cbor_len = cbor_encoder_get_buffer_size(&reply_encoder, cbor);
+ JADE_ASSERT(cbor_len && cbor_len <= sizeof(cbor));
+
+ // QR will now display, reply OK
+ jade_process_reply_to_message_ok(process);
+ JADE_LOGI("Success");
+
+ show_bip85_bip39_entropy_qr(cbor, cbor_len);
+
+cleanup:
+ SENSITIVE_POP(cbor);
+}
#endif // AMALGAMATED_BUILD
diff --git a/main/process/process_utils.c b/main/process/process_utils.c
index 66756d2..cd698b7 100644
--- a/main/process/process_utils.c
+++ b/main/process/process_utils.c
@@ -8,6 +8,7 @@
#include "../rsa.h"
#include "../ui.h"
#include "../utils/cbor_rpc.h"
+#include "../utils/util.h"
#include "process_utils.h"
@@ -128,7 +129,7 @@ bool params_identity_curve_index(CborValue* params, const char** identity, size_
// index is optional
if (rpc_has_field_data("index", params)) {
- if (!rpc_get_sizet("index", params, index)) {
+ if (!rpc_get_sizet("index", params, index) || *index > BIP32_MAX_CHILD_INDEX) {
*errmsg = "Failed to extract valid index from parameters";
return false;
}
@@ -422,7 +423,7 @@ bool params_get_bip85_rsa_key(CborValue* params, size_t* key_bits, size_t* index
return false;
}
- if (!rpc_get_sizet("index", params, index)) {
+ if (!rpc_get_sizet("index", params, index) || *index > BIP32_MAX_CHILD_INDEX) {
*errmsg = "Failed to fetch valid index from message";
return false;
}
diff --git a/main/qrmode.c b/main/qrmode.c
index 3d1ab37..aa5cb97 100644
--- a/main/qrmode.c
+++ b/main/qrmode.c
@@ -941,7 +941,6 @@ static gui_activity_t* create_display_bcur_qr_activity(const char* message[], co
return make_show_qr_activity(message, message_size, icons, num_icons, frames_per_qr, show_options_button);
}
-// Display a QR code, with access to size/speed options
static void display_bcur_qr(const char* message[], const size_t message_size, const char* bcur_type,
const uint8_t* cbor, const size_t cbor_len, const char* help_url)
{
@@ -1151,10 +1150,17 @@ cleanup:
return ret;
}
+void show_bip85_bip39_entropy_qr(const uint8_t* cbor, const size_t cbor_len)
+{
+ JADE_ASSERT(cbor && cbor_len);
+ const char* message[] = { "Scan with", "wallet", "app" };
+ display_bcur_qr(message, 3, BCUR_TYPE_JADE_BIP8539_REPLY, cbor, cbor_len, "blkstrm.com/bip85");
+}
+
+// Returns false if an error occured or the user cancelled the action
static bool handle_bip85_bip39_request_qr(const uint8_t* cbor, const size_t cbor_len)
{
- JADE_ASSERT(cbor);
- JADE_ASSERT(cbor_len);
+ JADE_ASSERT(cbor && cbor_len);
// Parse cbor
CborValue root;
@@ -1166,28 +1172,30 @@ static bool handle_bip85_bip39_request_qr(const uint8_t* cbor, const size_t cbor
return false;
}
- uint8_t cbor_reply[176]; // sufficient
+ const char* errmsg = NULL;
+ uint8_t reply_cbor[176]; // sufficient for encrypted bip85 reply
+ SENSITIVE_PUSH(reply_cbor, sizeof(reply_cbor));
+
CborEncoder reply_encoder;
- cbor_encoder_init(&reply_encoder, cbor_reply, sizeof(cbor_reply), 0);
+ cbor_encoder_init(&reply_encoder, reply_cbor, sizeof(reply_cbor), 0);
- const char* errmsg = NULL;
const int errcode = get_bip85_bip39_entropy_cbor(&root, &reply_encoder, &errmsg);
if (errcode) {
if (errcode != CBOR_RPC_USER_CANCELLED) {
JADE_LOGE("Error generating encrypted bip85 entropy: %s", errmsg);
- const char* message[] = { "Error in bip85/bip39", errmsg };
+ const char* message[] = { "Error generating entropy", errmsg };
await_error_activity(message, 2);
}
+ // An error occurred, or the user cancelled the action
+ SENSITIVE_POP(reply_cbor);
return false;
}
- const size_t reply_cbor_len = cbor_encoder_get_buffer_size(&reply_encoder, cbor_reply);
- JADE_ASSERT(reply_cbor_len && reply_cbor_len <= sizeof(cbor_reply));
-
- // Now display bcur QR
- const char* message[] = { "Scan with", "wallet", "app" };
- display_bcur_qr(message, 3, BCUR_TYPE_JADE_BIP8539_REPLY, cbor_reply, reply_cbor_len, "blkstrm.com/bip85");
+ const size_t reply_cbor_len = cbor_encoder_get_buffer_size(&reply_encoder, reply_cbor);
+ JADE_ASSERT(reply_cbor_len && reply_cbor_len <= sizeof(reply_cbor));
+ show_bip85_bip39_entropy_qr(reply_cbor, reply_cbor_len);
+ SENSITIVE_POP(reply_cbor);
return true;
}
diff --git a/main/qrmode.h b/main/qrmode.h
index ead29a8..6e7f93a 100644
--- a/main/qrmode.h
+++ b/main/qrmode.h
@@ -5,6 +5,8 @@
#include <stddef.h>
#include <stdint.h>
+#include <cbor.h>
+
#include "otpauth.h"
// NOTE: Jade only supports the bip39 English wordlist,
@@ -28,6 +30,9 @@ void handle_scan_qr(void);
bool display_bcur_bytes_qr(
const char* message[], size_t message_size, const uint8_t* data, size_t data_len, const char* help_url);
+// Display bip85/bip39 encrypted entropy as BC-UR QR.
+void show_bip85_bip39_entropy_qr(const uint8_t* cbor, const size_t cbor_len);
+
// Display screen with qr code
// Handles up to v6. codes - ie text up to 134 bytes
void await_single_qr_activity(const char* message[], size_t message_size, const uint8_t* data, size_t data_len);
diff --git a/main/utils/util.h b/main/utils/util.h
index 076a447..c4f1760 100644
--- a/main/utils/util.h
+++ b/main/utils/util.h
@@ -105,6 +105,8 @@ bool parse_uint64(const char* str, size_t str_len, uint64_t* value_out);
bool parse_uint32(const char* str, size_t str_len, uint32_t* value_out);
// Bip32 path utils
+#define BIP32_MAX_CHILD_INDEX 0x7fffffff
+
static inline bool ishardened(const uint32_t n) { return n & 0x80000000; }
static inline uint32_t harden(const uint32_t n) { return n | 0x80000000; }
static inline uint32_t unharden(const uint32_t n) { return n & ~0x80000000; }
Why this scored 24/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.