versioninfo: add JADE_INTERFACE field to show which connection source holds the unlocked wallet
What changed, and why it matters
This commit adds a new read-only field, JADE_INTERFACE, to the device's version information reply. It simply tells a connected computer which communication path (USB, Bluetooth, QR mode, etc.) currently has control of the unlocked wallet. There is no change to security rules, authentication, or how wallets are unlocked; it only exposes existing internal state for better visibility.
No security action required. Treat as a normal feature/visibility improvement. Reviewers may optionally confirm that keychain_get_userdata() reliably returns the expected source value in all build configurations.
Security signals we found
No authentication or authorization logic changed
No cryptographic or key-management code changed
No input parsing or memory-unsafe code added
New field is read-only and derived from existing internal state
Test coverage added for new field behavior
Evidence from the diff
The patch extends build_version_info_reply() in main/versioninfo.c to include a new CBOR string field ‘JADE_INTERFACE’. The value is derived from keychain_get_userdata() mapped through a new get_interface_name() helper over jade_msg_source_t values (SOURCE_INTERNAL/QR, SOURCE_SERIAL, SOURCE_BLE, SOURCE_QEMU_TCP, SOURCE_LIBJADE). Documentation and test expectations are updated accordingly, including a new test verifying the field reflects the active connection when unlocked and ‘NONE’ after logout. No logic governing access, locking, or key handling is modified.
Changed components
main/versioninfo.cdocs/index.rsttest_jade.pytests/rpc/test_low_level.pytests/rpc/test_pin_actions.pytests/rpc/test_ping.pyInspect captured patch +66 / −5
### docs/index.rst
@@ -151,6 +151,7 @@ get_version_info reply
"EFUSEMAC": "246F288F6364",
"BATTERY_STATUS": 5,
"JADE_STATE": "LOCKED",
+ "JADE_INTERFACE": "NONE",
"JADE_NETWORKS': "MAIN",
"JADE_HAS_PIN": true
}
@@ -166,6 +167,14 @@ get_version_info reply
- 'READY' - wallet set and unlocked for this interface, ready to use.
- 'TEMP' - hw currently set with a temporary ('Emergency Restore') wallet, ready to use.
+* 'JADE_INTERFACE' : the connection interface which currently holds the unlocked wallet, if any.
+ While a wallet is held by one interface it cannot be used over another
+
+ - 'NONE' - wallet is not currently held by any connection interface (so can be unlocked)
+ - 'QR' - wallet is held by the on-device QR interface (eg. a temporary 'QR Mode' wallet)
+ - 'SERIAL' - wallet is held by the USB/serial connection
+ - 'BLE' - wallet is held by the Bluetooth connection
+
* 'JADE_NETWORKS' :
- 'MAIN' - wallet is locked to mainnet/production networks and cannot be used on testnet or regtest networks.
### main/versioninfo.c
@@ -24,6 +24,26 @@ extern esp_app_desc_t running_app_info;
extern esp_chip_info_t chip_info;
extern uint8_t macid[6];
+static const char* get_interface_name(const jade_msg_source_t source)
+{
+ switch (source) {
+ case SOURCE_INTERNAL:
+ return "QR";
+ case SOURCE_SERIAL:
+ return "SERIAL";
+ case SOURCE_QEMU_TCP:
+ return "QEMU_TCP";
+ case SOURCE_BLE:
+ return "BLE";
+#ifdef CONFIG_LIBJADE
+ case SOURCE_LIBJADE:
+ return "LIBJADE";
+#endif
+ default:
+ return "NONE";
+ }
+}
+
void build_version_info_reply(const void* ctx, CborEncoder* container)
{
JADE_ASSERT(ctx);
@@ -32,9 +52,9 @@ void build_version_info_reply(const void* ctx, CborEncoder* container)
const jade_msg_source_t* const source = (const jade_msg_source_t*)ctx;
#ifdef CONFIG_DEBUG_MODE
- const uint8_t num_version_fields = 22;
+ const uint8_t num_version_fields = 23;
#else
- const uint8_t num_version_fields = 15;
+ const uint8_t num_version_fields = 16;
#endif
CborEncoder map_encoder;
@@ -110,6 +130,10 @@ void build_version_info_reply(const void* ctx, CborEncoder* container)
add_string_to_map(&map_encoder, "JADE_STATE", has_pin ? "LOCKED" : "UNINIT");
}
+ // The connection interface which currently holds the (unlocked) wallet, if any.
+ // While a wallet is held by one interface it cannot be used over another
+ add_string_to_map(&map_encoder, "JADE_INTERFACE", get_interface_name((jade_msg_source_t)keychain_get_userdata()));
+
const network_type_t restriction = keychain_get_network_type_restriction();
const char* networks = restriction == NETWORK_TYPE_MAIN ? "MAIN"
: restriction == NETWORK_TYPE_TEST ? "TEST"
### test_jade.py
@@ -176,7 +176,7 @@ def _get_test_cases(pattern, allow_sampling=True):
STALE_DATA_TIMEOUT = 4
# The number of values expected back in version info
-NUM_VALUES_VERINFO = 22
+NUM_VALUES_VERINFO = 23
ESP32S3_CHIP_BOARDS = ['JADE_V2', 'JADE_V2C', 'TTGO_TDISPLAYS3', 'TTGO_TDISPLAYS3PROCAMERA',
'M5CORES3']
### tests/rpc/test_low_level.py
@@ -11,7 +11,7 @@
# This value must be > TIMEOUT_TICKS to reliably trigger the stale path.
STALE_DATA_TIMEOUT = 4
-NUM_VALUES_VERINFO = 22
+NUM_VALUES_VERINFO = 23
LOW_LEVEL_VECTORS = next(_get_test_cases('tests/rpc/data/low_level/bad_params.json'))[0]
LOW_LEVEL_LIQUID_VECTORS = next(_get_test_cases(
### tests/rpc/test_pin_actions.py
@@ -189,6 +189,34 @@ def test_pin_requires_auth_user(jade):
assert reply['error']['message'] == 'Unexpected method'
+@pytest.mark.mnemonic(mnemonics.reset)
+def test_version_info_interface(pin_test_setup):
+ """Check 'JADE_INTERFACE' identifies the connection holding the unlocked wallet"""
+ jade, _, _ = pin_test_setup
+
+ config = get_jade_config()
+ if config.transport == 'ble':
+ expected_interface = 'BLE'
+ elif config.transport == 'libjade':
+ expected_interface = 'SERIAL'
+ elif config.device.startswith('tcp:'):
+ expected_interface = 'QEMU_TCP'
+ else:
+ expected_interface = 'SERIAL'
+
+ # The wallet is unlocked/held by this connection (pin_test_setup sets a pin and
+ # then calls auth_user)
+ info = jade.get_version_info()
+ assert info['JADE_STATE'] == 'READY'
+ assert info['JADE_INTERFACE'] == expected_interface
+
+ # Once logged out the wallet is no longer held by any connection
+ assert jade.logout() is True
+ info = jade.get_version_info()
+ assert info['JADE_STATE'] == 'LOCKED'
+ assert info['JADE_INTERFACE'] == 'NONE'
+
+
@pytest.mark.mnemonic(mnemonics.reset)
def test_bad_pin(pin_test_setup):
"""Check that Jade rejects bad PINs passed to 'debug_set_pin'"""
### tests/rpc/test_ping.py
@@ -1,7 +1,7 @@
from . import *
-NUM_VALUES_VERINFO = 22
+NUM_VALUES_VERINFO = 23
def test_ping_protocol(jade):Why this scored 19/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.