otp: parse google authenticator export qr codes
What changed, and why it matters
This commit adds the ability for the Blockstream Jade hardware wallet to read Google Authenticator's bulk-export QR codes (the 'otpauth-migration://' format). It converts those exported codes back into normal OTP URIs and registers them. The change is a feature addition, not a documented security fix. Because it parses an external, attacker-controllable protobuf payload and builds URIs from it, there is some potential for parsing bugs or malformed data to cause crashes or unexpected behavior, but the commit itself does not obviously introduce a known vulnerability.
Treat as a feature commit rather than a security patch. If auditing, focus on the new protobuf decode path: verify nanopb callback bounds, ensure JADE_MALLOC failures are handled safely, confirm URL-encoded values cannot overflow OTP_MAX_URI_LEN, and fuzz the otpauth-migration parser. No immediate patching is indicated by the diff alone.
Security signals we found
New parser for attacker-controllable QR/protobuf data
URL decoding and base64 decoding of untrusted input
Manual URI construction with memmove/memcpy/snprintf
Fixed-size buffers for protobuf string fields (64 bytes) and URI length limits
Rejection of unknown enum values for OTP type, algorithm, and digit count
No explicit security relevance or fix described by vendor
No CVE or advisory references in commit or supplied materials
Evidence from the diff
The patch introduces protobuf-based decoding of Google Authenticator migration payloads in main/otpauth.c. It parses otpauth-migration://offline?data=… URIs, base64-decodes the data parameter, URL-decodes it, then uses nanopb to decode a MigrationPayload protobuf containing multiple OTP parameters. Callbacks convert each record into a standard otpauth:// URI (type, secret, name, issuer, algorithm, digits, counter). The new flow is wired into QR scanning (register_otp_qr) and generic QR byte handling (qrmode.c). The commit adds bounds checks, uses JADE_MALLOC/JADE_CALLOC, marks buffers sensitive, and rejects unexpected URL fields, unknown OTP types, algorithms, and digit counts. No CVE, advisory, or vendor security statement is present in the supplied materials.
Changed components
main/otpauth.cmain/otpauth.hmain/process/register_otp.cmain/qrmode.cInspect captured patch +518 / −60
diff --git a/main/otpauth.c b/main/otpauth.c
index 64bb0e7..7d2e61a 100644
--- a/main/otpauth.c
+++ b/main/otpauth.c
@@ -6,10 +6,14 @@
#include "keychain.h"
#include "sensitive.h"
#include "storage.h"
+#include "utils/malloc_ext.h"
+#include "utils/urldecode.h"
#include "utils/util.h"
+#include <google-otpauth-migration.pb.h>
#include <http_parser.h>
#include <mbedtls/md.h>
+#include <pb_decode.h>
#include <stdint.h>
#include <stdio.h>
@@ -19,6 +23,9 @@
#define MBEDTLS_SHA512_HMAC_LEN 64
#define SECRET_BUFSIZE 256
+// Max byte length of a protobuf string field (name/issuer) in an OTP migration payload
+#define OTP_MIGRATE_PB_FIELD_LEN 64
+
static const uint8_t OTP_HMAC_KEY[] = { 'O', 'T', 'P', 's', 'e', 'e', 'd' };
// Check current timestamp is after Jan 01 2020 (just a sanity check that we have set the clock)
@@ -204,6 +211,378 @@ bool otp_uri_to_ctx(const char* uri, size_t uri_len, otpauth_ctx_t* otp_ctx)
return otp_is_valid(otp_ctx);
}
+#define OTP_MIGRATE_SCHEMA_OFFSET 8
+#define OTP_MIGRATE_REQ_FIELDS ((1 << UF_SCHEMA) | (1 << UF_HOST) | (1 << UF_QUERY))
+#define OTP_MIGRATE_INVALID_FIELDS ((1 << UF_PORT) | (1 << UF_FRAGMENT))
+
+// NOTE: 'data' ownership is assigned to the caller who must free after use
+static bool otp_migrate_url_to_data(const char* uri, size_t uri_len, uint8_t** data, size_t* data_len)
+{
+ JADE_ASSERT(uri);
+ JADE_INIT_OUT_PPTR(data);
+ JADE_INIT_OUT_SIZE(data_len);
+
+ // http_parser_parse_url() does not like non-alphabetic chars in the schema
+ if (strncmp(uri, OTP_MIGRATE_SCHEMA, sizeof(OTP_MIGRATE_SCHEMA) - 1) == 0) {
+ // change schema to remove '-' char
+ uri += OTP_MIGRATE_SCHEMA_OFFSET;
+ uri_len -= OTP_MIGRATE_SCHEMA_OFFSET;
+ }
+
+ struct http_parser_url u;
+ http_parser_url_init(&u);
+ OTP_CHECK_BOOL_RETURN(http_parser_parse_url(uri, uri_len, 0, &u) == 0);
+
+ if (u.field_data[UF_SCHEMA].len != 9
+ || strncmp(OTP_MIGRATE_SCHEMA + OTP_MIGRATE_SCHEMA_OFFSET, uri + u.field_data[UF_SCHEMA].off,
+ u.field_data[UF_SCHEMA].len)) {
+ JADE_LOGE("otp migrate uri missing expected " OTP_MIGRATE_SCHEMA_FULL " schema");
+ return false;
+ }
+
+ OTP_CHECK_BOOL_RETURN((u.field_set & OTP_MIGRATE_REQ_FIELDS) == OTP_MIGRATE_REQ_FIELDS);
+ OTP_CHECK_BOOL_RETURN(!(u.field_set & OTP_MIGRATE_INVALID_FIELDS));
+
+ // 'offline' string is in the 'host' position
+ if (u.field_data[UF_HOST].len != 7
+ || strncmp(OTP_MIGRATE_HOST, uri + u.field_data[UF_HOST].off, u.field_data[UF_HOST].len)) {
+ JADE_LOGE("otp migrate uri missing expected " OTP_MIGRATE_HOST " host");
+ return false;
+ }
+
+ // Remaining field is in the query/parameters string
+ const char* query = uri + u.field_data[UF_QUERY].off;
+ const size_t query_len = u.field_data[UF_QUERY].len;
+
+ // 'data' is mandatory
+ const char* data_param = NULL;
+ size_t data_param_len = 0;
+ OTP_CHECK_BOOL_RETURN(get_query_argument(query, query_len, "data", &data_param, &data_param_len));
+ OTP_CHECK_BOOL_RETURN(data_param && data_param_len);
+
+ // urldecode the data query parameter
+ char* data_b64 = JADE_MALLOC(data_param_len + 1);
+ SENSITIVE_PUSH(data_b64, data_param_len + 1);
+ if (!urldecode(data_param, data_param_len, data_b64, data_param_len + 1)) {
+ JADE_LOGE("Failed to urldecode otp migrate data");
+ SENSITIVE_POP(data_b64);
+ free(data_b64);
+ return false;
+ }
+ size_t data_b64_len = strlen(data_b64);
+
+ // convert from base64
+ size_t data_max_len;
+ JADE_WALLY_VERIFY(wally_base64_get_maximum_length(data_b64, 0, &data_max_len));
+ *data = JADE_MALLOC(data_max_len);
+ JADE_WALLY_VERIFY(wally_base64_n_to_bytes(data_b64, data_b64_len, 0, *data, data_max_len, data_len));
+ SENSITIVE_POP(data_b64);
+ free(data_b64);
+
+ return true;
+}
+
+static bool otp_uri_insert(char* uri, const char* prefix, const char* value)
+{
+ const size_t prefix_len = strlen(prefix);
+ const size_t value_len = strlen(value);
+ const size_t uri_len = strlen(uri);
+ if (uri_len + value_len >= OTP_MAX_URI_LEN) {
+ return false;
+ }
+ memmove(uri + prefix_len + value_len, uri + prefix_len, uri_len - prefix_len + 1);
+ memcpy(uri + prefix_len, value, value_len);
+ return true;
+}
+
+static size_t pb_read_stream(pb_istream_t* stream, const char* name, uint8_t* buf, size_t buf_len)
+{
+ const size_t num_bytes = stream->bytes_left;
+ if (num_bytes > buf_len) {
+ JADE_LOGE("%s buffer too small", name);
+ return 0;
+ }
+ if (!pb_read(stream, buf, num_bytes)) {
+ JADE_LOGE("%s read failed", name);
+ return 0;
+ }
+ return num_bytes;
+}
+
+static bool append_uri_param(char* uri, const char* key, const char* value, bool first)
+{
+ const size_t uri_len = strlen(uri);
+ int n = snprintf(uri + uri_len, OTP_MAX_URI_LEN - uri_len, "%c%s=%s", first ? '?' : '&', key, value);
+ if (n < 0 || n >= OTP_MAX_URI_LEN - uri_len) {
+ return false;
+ }
+ return true;
+}
+
+static bool decode_secret_fn(pb_istream_t* stream, const pb_field_t* field, void** arg)
+{
+ uint8_t buf[32];
+ SENSITIVE_PUSH(buf, sizeof(buf));
+ const size_t buf_len = pb_read_stream(stream, "secret", buf, sizeof(buf));
+ if (!buf_len) {
+ SENSITIVE_POP(buf);
+ return false;
+ }
+
+ // convert to base32
+ char base32[sizeof(buf) * 2];
+ SENSITIVE_PUSH(base32, sizeof(base32));
+ const bool use_padding = false; // padding not used for uris because '=' char is not url-safe
+ bool ret = bin_to_base32(buf, buf_len, base32, sizeof(base32), use_padding);
+ if (ret) {
+ // write to output arg
+ char* opt_uri = (char*)(*arg);
+ ret = append_uri_param(opt_uri, "secret", base32, true);
+ }
+ SENSITIVE_POP(base32);
+ SENSITIVE_POP(buf);
+ return ret;
+}
+
+typedef struct {
+ char* uri_out;
+ // Buffers to hold url-encoded name and issuer, populated during pb_decode.
+ // URI construction is deferred to after pb_decode to avoid field-ordering issues.
+ // https://protobuf.dev/programming-guides/encoding/#order
+ char name_encoded[OTP_MIGRATE_PB_FIELD_LEN * 3 + 1];
+ bool has_name;
+ bool name_has_colon;
+ char issuer_encoded[OTP_MIGRATE_PB_FIELD_LEN * 3 + 1];
+ bool has_issuer;
+} otp_migrate_decode_str_ctx_t;
+
+static bool decode_name_fn(pb_istream_t* stream, const pb_field_t* field, void** arg)
+{
+ uint8_t buf[OTP_MIGRATE_PB_FIELD_LEN];
+ const size_t buf_len = pb_read_stream(stream, "name", buf, sizeof(buf) - 1);
+ if (!buf_len) {
+ return false;
+ }
+ buf[buf_len] = '\0';
+
+ JADE_LOGI("Decoded name: %s", buf);
+
+ otp_migrate_decode_str_ctx_t* str_ctx = (otp_migrate_decode_str_ctx_t*)(*arg);
+
+ // check if name contains a ':' character, in which case we will not prefix the name
+ // with the issuer later on
+ str_ctx->name_has_colon = memchr(buf, ':', buf_len) != NULL;
+
+ // uriencode the name value and store for post-decode URI construction
+ if (!urlencode((char*)buf, buf_len, str_ctx->name_encoded, sizeof(str_ctx->name_encoded))) {
+ return false;
+ }
+ str_ctx->has_name = true;
+ return true;
+}
+
+static bool decode_issuer_fn(pb_istream_t* stream, const pb_field_t* field, void** arg)
+{
+ uint8_t buf[OTP_MIGRATE_PB_FIELD_LEN];
+ const size_t buf_len = pb_read_stream(stream, "issuer", buf, sizeof(buf) - 1);
+ if (!buf_len) {
+ return false;
+ }
+ buf[buf_len] = '\0';
+
+ JADE_LOGI("Decoded issuer: %s", buf);
+
+ otp_migrate_decode_str_ctx_t* str_ctx = (otp_migrate_decode_str_ctx_t*)(*arg);
+
+ // uriencode the issuer value and store for post-decode URI construction
+ if (!urlencode((char*)buf, buf_len, str_ctx->issuer_encoded, sizeof(str_ctx->issuer_encoded))) {
+ return false;
+ }
+ str_ctx->has_issuer = true;
+ return true;
+}
+
+static bool decode_otp_parameters_fn(pb_istream_t* stream, const pb_field_t* field, void** arg)
+{
+ otpauth_migrate_ctx_t* ctx = (otpauth_migrate_ctx_t*)(*arg);
+
+ // find first empty slot in output array
+ size_t i;
+ for (i = 0; i < ctx->uris_out_len; ++i) {
+ if (ctx->uris_out[i] == NULL) {
+ break;
+ }
+ }
+ if (i == ctx->uris_out_len) {
+ JADE_LOGE("Too many OTP records in migration data, max supported is %zu", ctx->uris_out_len);
+ return false;
+ }
+
+ // allocate output URI buffer
+ ctx->uris_out[i] = JADE_MALLOC(OTP_MAX_URI_LEN);
+ char* uri_out = ctx->uris_out[i];
+
+ // start with schema
+ const int ret = snprintf(uri_out, OTP_MAX_URI_LEN, OTP_SCHEMA_FULL);
+ JADE_ASSERT(ret > 0 && ret < OTP_MAX_URI_LEN);
+
+ // decode the message into the URI
+ MigrationPayload_OtpParameters message = MigrationPayload_OtpParameters_init_zero;
+ message.secret.funcs.decode = decode_secret_fn;
+ message.secret.arg = uri_out;
+ otp_migrate_decode_str_ctx_t str_ctx = { .uri_out = uri_out,
+ .name_encoded = { 0 },
+ .has_name = false,
+ .name_has_colon = false,
+ .issuer_encoded = { 0 },
+ .has_issuer = false };
+ message.name.funcs.decode = decode_name_fn;
+ message.name.arg = &str_ctx;
+ message.issuer.funcs.decode = decode_issuer_fn;
+ message.issuer.arg = &str_ctx;
+ if (!pb_decode(stream, MigrationPayload_OtpParameters_fields, &message)) {
+ JADE_LOGE("otp migrate data decode failed");
+ return false;
+ }
+
+ // Insert name and issuer into URI now that all fields are decoded, avoiding
+ // field-ordering issues that might arise if done inside the individual callbacks.
+ if (str_ctx.has_name) {
+ if (!otp_uri_insert(uri_out, OTP_SCHEMA_FULL, str_ctx.name_encoded)) {
+ return false;
+ }
+ }
+ if (str_ctx.has_issuer) {
+ if (!str_ctx.name_has_colon) {
+ // prefix label with "issuer:" to form "otpauth://<type>/<issuer>:<name>?..."
+ if (!otp_uri_insert(uri_out, OTP_SCHEMA_FULL, ":")) {
+ return false;
+ }
+ if (!otp_uri_insert(uri_out, OTP_SCHEMA_FULL, str_ctx.issuer_encoded)) {
+ return false;
+ }
+ }
+ if (!append_uri_param(uri_out, "issuer", str_ctx.issuer_encoded, false)) {
+ return false;
+ }
+ }
+
+ // insert OTP type
+ switch (message.type) {
+ case MigrationPayload_OtpType_OTP_TYPE_HOTP:
+ if (!otp_uri_insert(uri_out, OTP_SCHEMA_FULL, "hotp/")) {
+ return false;
+ }
+ break;
+ case MigrationPayload_OtpType_OTP_TYPE_TOTP:
+ if (!otp_uri_insert(uri_out, OTP_SCHEMA_FULL, "totp/")) {
+ return false;
+ }
+ break;
+ default:
+ JADE_LOGE("Unsupported OTP type: %d", (int)message.type);
+ return false;
+ }
+
+ // add algorithm if not default (SHA1)
+ if (message.algorithm != MigrationPayload_Algorithm_ALGORITHM_SHA1) {
+ const char* algo = NULL;
+ switch (message.algorithm) {
+ case MigrationPayload_Algorithm_ALGORITHM_SHA256:
+ algo = "SHA256";
+ break;
+ case MigrationPayload_Algorithm_ALGORITHM_SHA512:
+ algo = "SHA512";
+ break;
+ case MigrationPayload_Algorithm_ALGORITHM_MD5:
+ algo = "MD5";
+ break;
+ default:
+ JADE_LOGE("Unsupported algorithm: %d", (int)message.algorithm);
+ return false;
+ }
+ if (!append_uri_param(uri_out, "algorithm", algo, false)) {
+ return false;
+ }
+ }
+
+ // add digits if not default (6)
+ if (message.digits != MigrationPayload_DigitCount_DIGIT_COUNT_SIX) {
+ switch (message.digits) {
+ case MigrationPayload_DigitCount_DIGIT_COUNT_EIGHT:
+ if (!append_uri_param(uri_out, "digits", "8", false)) {
+ return false;
+ }
+ break;
+ default:
+ JADE_LOGE("Unsupported digit count: %d", (int)message.digits);
+ return false;
+ }
+ }
+
+ // add counter if HOTP and counter > 0
+ if (message.type == MigrationPayload_OtpType_OTP_TYPE_HOTP && message.counter > 0) {
+ char counter_str[21]; // Big enough to hold 64-bit integer
+ const int ret = snprintf(counter_str, sizeof(counter_str), "%" PRId64, message.counter);
+ JADE_ASSERT(ret > 0 && ret < sizeof(counter_str));
+ if (!append_uri_param(uri_out, "counter", counter_str, false)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+void otp_migrate_uri_to_ctx_free(otpauth_migrate_ctx_t* ctx)
+{
+ JADE_ASSERT(ctx && ctx->uris_out);
+ for (size_t i = 0; i < ctx->uris_out_len; ++i) {
+ if (ctx->uris_out[i]) {
+ JADE_WALLY_VERIFY(wally_free_string(ctx->uris_out[i]));
+ ctx->uris_out[i] = NULL;
+ }
+ }
+ free(ctx->uris_out);
+ ctx->uris_out = NULL;
+ ctx->uris_out_len = 0;
+}
+
+bool otp_migrate_uri_to_ctx(const char* uri, const size_t uri_len, const size_t max_uris, otpauth_migrate_ctx_t* ctx)
+{
+ JADE_ASSERT(uri && uri_len);
+ JADE_ASSERT(max_uris);
+ JADE_ASSERT(ctx);
+
+ // Initialize the context
+ ctx->uris_out = JADE_CALLOC(max_uris, sizeof(char*));
+ ctx->uris_out_len = max_uris;
+
+ // convert the otpauth-migration data param (base64) into binary data
+ uint8_t* data;
+ size_t data_len;
+ OTP_CHECK_BOOL_RETURN(otp_migrate_url_to_data(uri, uri_len, &data, &data_len));
+ SENSITIVE_PUSH(data, data_len);
+
+ bool result = false;
+
+ // protobuf decode
+ MigrationPayload message = MigrationPayload_init_zero;
+ message.otp_parameters.funcs.decode = decode_otp_parameters_fn;
+ message.otp_parameters.arg = ctx;
+ pb_istream_t stream = pb_istream_from_buffer(data, data_len);
+ if (!pb_decode(&stream, MigrationPayload_fields, &message)) {
+ JADE_LOGE("otp migrate data decode failed");
+ goto cleanup;
+ }
+
+ result = true;
+
+cleanup:
+ SENSITIVE_POP(data);
+ free(data);
+
+ return result;
+}
+
void otp_set_explicit_value(otpauth_ctx_t* otp_ctx, const int64_t value)
{
JADE_ASSERT(otp_is_valid(otp_ctx));
diff --git a/main/otpauth.h b/main/otpauth.h
index 86c0768..6dbdb55 100644
--- a/main/otpauth.h
+++ b/main/otpauth.h
@@ -13,6 +13,9 @@
#define OTP_SCHEMA "otpauth"
#define OTP_SCHEMA_FULL "otpauth://"
+#define OTP_MIGRATE_SCHEMA "otpauth-migration"
+#define OTP_MIGRATE_SCHEMA_FULL "otpauth-migration://"
+#define OTP_MIGRATE_HOST "offline"
typedef struct otpauth_ctx {
uint64_t counter;
@@ -33,6 +36,13 @@ typedef struct otpauth_ctx {
int8_t period;
} otpauth_ctx_t;
+typedef struct otpauth_migrate_decode_ctx {
+ // otpauth:// uris for each record decoded from the migrate uri
+ char** uris_out;
+ // length of the 'uris_out' array (ie. max number of uris to decode)
+ size_t uris_out_len;
+} otpauth_migrate_ctx_t;
+
typedef enum { OTP_ERR_OK, OTP_ERR_TOTP_TIME, OTP_ERR_HOTP_COUNTER } otp_err_t;
bool otp_is_valid(const otpauth_ctx_t* otp_ctx);
@@ -40,6 +50,11 @@ bool otp_is_valid(const otpauth_ctx_t* otp_ctx);
// Parse the otp uri into a context object
bool otp_uri_to_ctx(const char* uri, size_t uri_len, otpauth_ctx_t* otp_ctx);
+// Parse the otp migrate uri into a context object
+bool otp_migrate_uri_to_ctx(const char* uri, size_t uri_len, size_t max_uris, otpauth_migrate_ctx_t* ctx);
+// Free the context object from parsing (whether successful or not)
+void otp_migrate_uri_to_ctx_free(otpauth_migrate_ctx_t* ctx);
+
// Update the context object with an explicit or default/calculated nonce value
void otp_set_explicit_value(otpauth_ctx_t* otp_ctx, int64_t value);
otp_err_t otp_set_default_value(otpauth_ctx_t* otp_ctx, uint64_t* value_out);
diff --git a/main/process/register_otp.c b/main/process/register_otp.c
index 387ffa9..74cea65 100644
--- a/main/process/register_otp.c
+++ b/main/process/register_otp.c
@@ -302,22 +302,23 @@ static bool validate_scanned_otp_uri(qr_data_t* qr_data)
JADE_ASSERT(qr_data->len <= sizeof(qr_data->data));
JADE_ASSERT(qr_data->data[qr_data->len] == '\0');
- if (qr_data->len >= OTP_MAX_URI_LEN) {
- JADE_LOGW("String data from qr unexpectedly long: %u", qr_data->len);
- goto invalid_qr;
+ // Check if otpauth:// uri
+ if (qr_data->len < OTP_MAX_URI_LEN) {
+ otpauth_ctx_t otp_ctx = { .name = "otp_scanning" };
+ if (otp_uri_to_ctx((const char*)qr_data->data, qr_data->len, &otp_ctx)) {
+ // Looks like a valid otp uri
+ return true;
+ }
}
- otpauth_ctx_t otp_ctx = { .name = "otp_scanning" };
- if (!otp_uri_to_ctx((const char*)qr_data->data, qr_data->len, &otp_ctx)) {
- JADE_LOGW("Invalid otp uri string: %s", (const char*)qr_data->data);
- goto invalid_qr;
+ // Check if otpauth-migrate:// uri
+ otpauth_migrate_ctx_t otp_migrate_ctx;
+ if (otp_migrate_uri_to_ctx((const char*)qr_data->data, qr_data->len, 8, &otp_migrate_ctx)) {
+ // Looks like a valid otp migrate uri
+ otp_migrate_uri_to_ctx_free(&otp_migrate_ctx);
+ return true;
}
-
- // uri appears valid
- return true;
-
-invalid_qr:
- /* no-op */; // Need an empty statement to allow a label before a declaration
+ otp_migrate_uri_to_ctx_free(&otp_migrate_ctx);
// Show the user that a valid qr was scanned, but the string data
// did not constitute a valid/parseable OTP URI string.
@@ -330,29 +331,27 @@ invalid_qr:
return false;
}
-bool register_otp_qr(void)
+int register_otp_string(const char* otp_uri, const size_t uri_len, const char** errmsg)
{
+ JADE_ASSERT(otp_uri);
+ JADE_ASSERT(uri_len);
+ JADE_INIT_OUT_PPTR(errmsg);
JADE_ASSERT(keychain_get());
+ // Parse uri
+ otpauth_ctx_t otp_ctx = { .name = "otp_string" };
+ if (!otp_uri_to_ctx(otp_uri, uri_len, &otp_ctx)) {
+ *errmsg = "Failed to parse otp record";
+ return CBOR_RPC_INTERNAL_ERROR;
+ }
+
// Check keychain has seed data
if (keychain_get()->seed_len == 0) {
JADE_LOGE("No wallet seed available. Wallet must be re-initialised from mnemonic.");
- const char* message[] = { "Feature requires Jade reset" };
+ *errmsg = "No wallet seed available";
+ const char* message[] = { "Feature requires Jade wallet" };
await_error_activity(message, 1);
- return false;
- }
-
- bool ret = false;
- const char* errmsg = NULL;
-
- qr_data_t qr_data = { .len = 0, .is_valid = validate_scanned_otp_uri };
- SENSITIVE_PUSH(&qr_data, sizeof(qr_data));
-
- // Get URI from qr code scan
- if (!jade_camera_scan_qr(&qr_data, NULL, QR_GUIDE_SHOW, "blkstrm.com/otp") || !qr_data.len) {
- // User exit without scanning
- JADE_LOGW("No qr code scanned");
- goto cleanup;
+ return CBOR_RPC_INTERNAL_ERROR;
}
// Get OTP Name (only) from kb
@@ -360,59 +359,110 @@ bool register_otp_qr(void)
if (!get_otp_data_from_kb(otp_name, sizeof(otp_name), NULL, 0, NULL)) {
// User abandoned
JADE_LOGW("User abandoned (entering otp name)");
- goto cleanup;
+ *errmsg = "User abandoned entering otp name";
+ return CBOR_RPC_USER_CANCELLED;
}
// Validate and persist the new otp uri
- const int errcode = handle_new_otp_uri(otp_name, (const char*)qr_data.data, qr_data.len, &errmsg);
- if (errcode && errcode != CBOR_RPC_USER_CANCELLED) {
- // Display any error (ignoring explicit user cancel)
- const char* message[] = { errmsg };
- await_error_activity(message, 1);
+ return handle_new_otp_uri(otp_name, otp_uri, uri_len, errmsg);
+}
+
+int register_otp_migrate_string(const char* otp_migrate_uri, size_t uri_len, const char** errmsg)
+{
+ JADE_ASSERT(otp_migrate_uri);
+ JADE_ASSERT(uri_len);
+ JADE_INIT_OUT_PPTR(errmsg);
+ JADE_ASSERT(keychain_get());
+
+ int ret = CBOR_RPC_INTERNAL_ERROR;
+
+ // Parse uri
+ otpauth_migrate_ctx_t otp_migrate_ctx;
+ if (!otp_migrate_uri_to_ctx(otp_migrate_uri, uri_len, 8, &otp_migrate_ctx)) {
+ *errmsg = "Failed to parse otp migrate record";
goto cleanup;
}
- // All good
- ret = true;
+ // Register each opt uri contained in the migrate uri
+ for (size_t i = 0; i < otp_migrate_ctx.uris_out_len; i++) {
+ const char* otp_uri = otp_migrate_ctx.uris_out[i];
+ if (!otp_uri) {
+ continue;
+ }
+
+ const int errcode = register_otp_string(otp_uri, strlen(otp_uri), errmsg);
+ if (errcode && errcode != CBOR_RPC_USER_CANCELLED) {
+ JADE_LOGE("Failed to register OTP record from migrate uri: %s", *errmsg);
+ goto cleanup;
+ }
+ }
+
+ ret = 0; // success
cleanup:
- SENSITIVE_POP(&qr_data);
+ otp_migrate_uri_to_ctx_free(&otp_migrate_ctx);
return ret;
}
-int register_otp_string(const char* otp_uri, const size_t uri_len, const char** errmsg)
+bool register_otp_qr(void)
{
- JADE_ASSERT(otp_uri);
- JADE_ASSERT(uri_len);
- JADE_INIT_OUT_PPTR(errmsg);
JADE_ASSERT(keychain_get());
- // Parse uri
- otpauth_ctx_t otp_ctx = { .name = "otp_string" };
- if (!otp_uri_to_ctx(otp_uri, uri_len, &otp_ctx)) {
- *errmsg = "Failed to parse otp record";
- return CBOR_RPC_INTERNAL_ERROR;
- }
-
// Check keychain has seed data
if (keychain_get()->seed_len == 0) {
JADE_LOGE("No wallet seed available. Wallet must be re-initialised from mnemonic.");
- *errmsg = "Failed to parse otp record";
const char* message[] = { "Feature requires Jade reset" };
await_error_activity(message, 1);
- return CBOR_RPC_INTERNAL_ERROR;
+ return false;
}
- // Get OTP Name (only) from kb
- char otp_name[OTP_MAX_NAME_LEN];
- if (!get_otp_data_from_kb(otp_name, sizeof(otp_name), NULL, 0, NULL)) {
- // User abandoned
- JADE_LOGW("User abandoned (entering otp name)");
- *errmsg = "User abandoned entering otp name";
- return CBOR_RPC_USER_CANCELLED;
+ bool ret = false;
+
+ qr_data_t qr_data = { .len = 0, .is_valid = validate_scanned_otp_uri };
+ SENSITIVE_PUSH(&qr_data, sizeof(qr_data));
+
+ // Get URI from qr code scan
+ if (!jade_camera_scan_qr(&qr_data, NULL, QR_GUIDE_SHOW, "blkstrm.com/otp") || !qr_data.len) {
+ // User exit without scanning
+ JADE_LOGW("No qr code scanned");
+ goto cleanup;
}
- // Validate and persist the new otp uri
- return handle_new_otp_uri(otp_name, otp_uri, uri_len, errmsg);
+ // Try to handle as otp string
+ if (qr_data.len > sizeof(OTP_SCHEMA_FULL)
+ && !strncasecmp((const char*)qr_data.data, OTP_SCHEMA_FULL, sizeof(OTP_SCHEMA_FULL) - 1)) {
+ // Looks like an OTP URI
+ const char* errmsg = NULL;
+ const int errcode = register_otp_string((const char*)qr_data.data, qr_data.len, &errmsg);
+ if (errcode && errcode != CBOR_RPC_USER_CANCELLED) {
+ JADE_LOGE("Processing OTP URI failed: %s", errmsg);
+ // Display any error (ignoring explicit user cancel)
+ const char* message[] = { errmsg };
+ await_error_activity(message, 1);
+ goto cleanup;
+ }
+ }
+
+ // Try to handle as otpauth-migrate string
+ if (qr_data.len > sizeof(OTP_MIGRATE_SCHEMA_FULL)
+ && !strncasecmp((const char*)qr_data.data, OTP_MIGRATE_SCHEMA_FULL, sizeof(OTP_MIGRATE_SCHEMA_FULL) - 1)) {
+ // Looks like an OTP MIGRATE URI
+ const char* errmsg = NULL;
+ const int errcode = register_otp_migrate_string((const char*)qr_data.data, qr_data.len, &errmsg);
+ if (errcode && errcode != CBOR_RPC_USER_CANCELLED) {
+ JADE_LOGE("Processing OTP MIGRATE URI failed: %s", errmsg);
+ // Display any error (ignoring explicit user cancel)
+ const char* message[] = { errmsg };
+ await_error_activity(message, 1);
+ goto cleanup;
+ }
+ }
+
+ // All good
+ ret = true;
+
+cleanup:
+ SENSITIVE_POP(&qr_data);
+ return ret;
}
#endif // AMALGAMATED_BUILD
diff --git a/main/qrmode.c b/main/qrmode.c
index 06a64e9..e54de41 100644
--- a/main/qrmode.c
+++ b/main/qrmode.c
@@ -81,6 +81,7 @@ gui_activity_t* make_qr_options_activity(gui_view_node_t** density_textbox, gui_
bool import_mnemonic(const uint8_t* bytes, size_t bytes_len, char* buf, size_t buf_len, size_t* written);
int register_otp_string(const char* otp_uri, size_t uri_len, const char** errmsg);
+int register_otp_migrate_string(const char* otp_uri, size_t uri_len, const char** errmsg);
int register_multisig_file(const char* multisig_file, size_t multisig_file_len, const char** errmsg);
int update_pinserver(const CborValue* const params, const char** errmsg);
int params_set_epoch_time(CborValue* params, const char** errmsg);
@@ -1032,6 +1033,19 @@ static bool handle_qr_bytes(const uint8_t* bytes, const size_t bytes_len)
return true;
}
+ // Try to handle as otpauth-migrate string
+ if (bytes_len > sizeof(OTP_MIGRATE_SCHEMA_FULL)
+ && !strncasecmp(strbytes, OTP_MIGRATE_SCHEMA_FULL, sizeof(OTP_MIGRATE_SCHEMA_FULL) - 1)) {
+ // Looks like an OTP MIGRATE URI
+ const char* errmsg = NULL;
+ const int errcode = register_otp_migrate_string(strbytes, bytes_len, &errmsg);
+ if (errcode) {
+ JADE_LOGE("Processing OTP MIGRATE URI failed: %s", errmsg);
+ return false;
+ }
+ return true;
+ }
+
// Try to handle as multisig file
if (strcasestr(strbytes, "Name") && strcasestr(strbytes, "Format") && strcasestr(strbytes, "Policy")
&& strcasestr(strbytes, "Derivation")) {
Why this scored 36/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.