Skip to content

signETHTypedMessage reports every device error as user cancellation #33

Description

@Danswar

Summary

Every failure mode of signETHTypedMessage — firmware rejection, transport error, malformed request, genuine user cancellation — is collapsed into an empty byte array, so callers can only report "the user cancelled". The real error is printed to stdout and then discarded.

The chain

1. Go — error text discarded (go/api/ethereum.go:201-206)

signature, err = bitbox.ETHSignTypedMessage(uint64(chainId), keypathData, jsonMsg)
if err != nil {
    fmt.Printf("[ETHSignTypedMessage] device error: %v\n", err)
    return nil
}

2. Swift — nil becomes empty Data() (ios/Classes/BitboxFlutterPlugin.swift:299-306)

let signature = ApiETHSignTypedMessage(chainId, keypathHex, jsonMessage.data)
DispatchQueue.main.async {
    if let sig = signature {
        result(FlutterStandardTypedData(bytes: sig))
    } else {
        result(FlutterStandardTypedData(bytes: Data()))
    }
}

3. Dart — empty passed through (lib/usb/bitbox_usb_method_channel.dart)

return result ?? Uint8List(0);

Consumers then have no signal other than "empty", and the only reasonable interpretation left to them is user cancellation.

The same pattern applies to signETHMessage (go/api/ethereum.go:185-187) and to the other nilData() branches in the plugin.

Impact

A real device error is indistinguishable from a user pressing cancel. Diagnosing an actual firmware rejection required attaching to the app's stdout with devicectl device process launch --console to recover the one line that was already being thrown away:

[ETHSignTypedMessage] device error: unexpected NACK response

Without that, the only visible symptom was a "signature cancelled, please confirm on the device again" message, which is actively misleading — the user had confirmed, and retrying could never succeed.

Suggested fix

Propagate the error instead of flattening it:

  • Go: return the error text alongside the signature (or expose a last-error accessor) rather than return nil
  • Swift: result(FlutterError(code:message:details:)) on failure instead of empty Data()
  • Dart: throw a typed exception carrying the device message; keep a distinct type for genuine user abort so callers can still special-case it

bitbox02-api-go already distinguishes the two — firmware.IsErrorAbort(err) identifies a real user abort (code 104) versus any other device or transport error — so the information needed to keep the cancellation case separate is available at the Go layer.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions