From f484ac834812ea3a433b5577bcc36711232c9cf4 Mon Sep 17 00:00:00 2001 From: NicolaLS Date: Wed, 12 Jun 2024 12:58:39 -0600 Subject: [PATCH] api/firmware/device: info use device status fields Return struct containing REQ_INFO response data from device.info(). Amongst the already existing fields that were previously returned by info() as a tuple, also add the initialized status to the struct that is returned after firmware version 9.20.0. In case the device does not respond with the initialized status byte yet it will be nil. Otherwise true/false depending on the initialized status (seeded and backup created) of the device. Also use the information from device.info to set the device.status earlier to avoid showing the password video if the device is not initialized. Also rename OP_INFO to REQ_INFO because the name changed after this commit: https://github.com/BitBoxSwiss/bitbox02-firmware/commit/a7850125728bb447a9b79d1032bcd82c7faf83fd The name should be the same as in the firmware repositroy so that the code base is easier to understand. Also rename StatusInitialized to StatusUnlocked to avoid confusion. --- api/firmware/backup.go | 6 +- api/firmware/backup_test.go | 2 +- api/firmware/device.go | 106 ++++++++++++++++++++++-------------- api/firmware/mnemonic.go | 4 +- api/firmware/pairing.go | 2 +- api/firmware/query.go | 2 +- api/firmware/status.go | 8 +-- api/firmware/system.go | 2 +- 8 files changed, 78 insertions(+), 54 deletions(-) diff --git a/api/firmware/backup.go b/api/firmware/backup.go index dd1ce85..c26c822 100644 --- a/api/firmware/backup.go +++ b/api/firmware/backup.go @@ -23,7 +23,7 @@ import ( // CreateBackup is called after SetPassword() to create the backup. func (device *Device) CreateBackup() error { - if device.status != StatusSeeded && device.status != StatusInitialized { + if device.status != StatusSeeded && device.status != StatusUnlocked { return errp.New("invalid status") } @@ -49,7 +49,7 @@ func (device *Device) CreateBackup() error { return errp.New("unexpected response") } if device.status == StatusSeeded { - device.changeStatus(StatusInitialized) + device.changeStatus(StatusUnlocked) } return nil } @@ -130,6 +130,6 @@ func (device *Device) RestoreBackup(id string) error { if !ok { return errp.New("unexpected response") } - device.changeStatus(StatusInitialized) + device.changeStatus(StatusUnlocked) return nil } diff --git a/api/firmware/backup_test.go b/api/firmware/backup_test.go index 9d23f25..f72290b 100644 --- a/api/firmware/backup_test.go +++ b/api/firmware/backup_test.go @@ -38,7 +38,7 @@ func TestSimulatorBackups(t *testing.T) { require.Error(t, err) require.NoError(t, device.CreateBackup()) - require.Equal(t, StatusInitialized, device.Status()) + require.Equal(t, StatusUnlocked, device.Status()) list, err = device.ListBackups() require.NoError(t, err) diff --git a/api/firmware/device.go b/api/firmware/device.go index 441e6f9..a807ad6 100644 --- a/api/firmware/device.go +++ b/api/firmware/device.go @@ -114,15 +114,28 @@ type DeviceInfo struct { SecurechipModel string `json:"securechipModel"` } +// info is the data returned from the REQ_INFO api call. +type info struct { + // Device firmware version. REQ_INFO is supported since v4.3.0 which means this field will + // always be at least v4.3.0. + version *semver.SemVer + // Device Platform/Edition e.g. "bitbox02-btconly". + product common.Product + // Device unlocked status, true if device is unlocked. + unlocked bool + // Device initialized status, true if device is seeded and backup has been stored. + initialized *bool +} + // NewDevice creates a new instance of Device. // version: // // Can be given if known at the time of instantiation, e.g. by parsing the USB HID product string. // It must be provided if the version could be less than 4.3.0. -// If nil, the version will be queried from the device using the OP_INFO api endpoint. Do this +// If nil, the version will be queried from the device using the REQ_INFO api endpoint. Do this // when you are sure the firmware version is bigger or equal to 4.3.0. // -// product: same deal as with the version, after 4.3.0 it can be inferred by OP_INFO. +// product: same deal as with the version, after 4.3.0 it can be inferred by REQ_INFO. func NewDevice( version *semver.SemVer, product *common.Product, @@ -143,26 +156,26 @@ func NewDevice( } } -// info uses the opInfo api endpoint to learn about the version, platform/edition, and unlock -// status (true if unlocked). -func (device *Device) info() (*semver.SemVer, common.Product, bool, error) { +// info uses the opInfo api endpoint to learn about the version, platform/edition, unlock +// status (true if unlocked), and initialized status (true if device can be unlocked/is unlocked). +func (device *Device) info() (*info, error) { // CAREFUL: hwwInfo is called on the raw transport, not on device.rawQuery, which behaves // differently depending on the firmware version. Reason: the version is not // available (this call is used to get the version), so it must work for all firmware versions. response, err := device.communication.Query([]byte(hwwInfo)) if err != nil { - return nil, "", false, err + return nil, err } - if len(response) < 4 { - return nil, "", false, errp.New("unexpected response") + if len(response) < 5 { + return nil, errp.New("unexpected response") } versionStrLen, response := int(response[0]), response[1:] versionBytes, response := response[:versionStrLen], response[versionStrLen:] version, err := semver.NewSemVerFromString(string(versionBytes)) if err != nil { - return nil, "", false, err + return nil, err } platformByte, response := response[0], response[1:] editionByte, response := response[0], response[1:] @@ -175,24 +188,40 @@ func (device *Device) info() (*semver.SemVer, common.Product, bool, error) { } editions, ok := products[platformByte] if !ok { - return nil, "", false, errp.Newf("unrecognized platform: %v", platformByte) + return nil, errp.Newf("unrecognized platform: %v", platformByte) } product, ok := editions[editionByte] if !ok { - return nil, "", false, errp.Newf("unrecognized platform/edition: %v/%v", platformByte, editionByte) + return nil, errp.Newf("unrecognized platform/edition: %v/%v", platformByte, editionByte) } var unlocked bool - unlockedByte := response[0] + unlockedByte, response := response[0], response[1:] switch unlockedByte { case 0x00: unlocked = false case 0x01: unlocked = true default: - return nil, "", false, errp.New("unexpected reply") + return nil, errp.New("unexpected reply") + } + + deviceInfo := info{ + version: version, + product: product, + unlocked: unlocked, } - return version, product, unlocked, nil + + // Since 9.20.0 REQ_INFO responds with a byte for the initialized status. + if version.AtLeast(semver.NewSemVer(9, 20, 0)) { + initialized := response[0] == 0x01 + if response[0] != 0x00 && response[0] != 0x01 { + return nil, errp.New("unexpected reply") + } + deviceInfo.initialized = &initialized + } + + return &deviceInfo, nil } // Version returns the firmware version. @@ -203,30 +232,6 @@ func (device *Device) Version() *semver.SemVer { return device.version } -// inferVersionAndProduct either sets the version and product by using OP_INFO if they were not -// provided. In this case, the firmware is assumed to be >=v4.3.0, before that OP_INFO was not -// available. -func (device *Device) inferVersionAndProduct() error { - // The version has not been provided, so we try to get it from OP_INFO. - if device.version == nil { - version, product, _, err := device.info() - if err != nil { - return errp.New( - "OP_INFO unavailable; need to provide version and product via the USB HID descriptor") - } - device.log.Info(fmt.Sprintf("OP_INFO: version=%s, product=%s", version, product)) - - // sanity check - if !version.AtLeast(semver.NewSemVer(4, 3, 0)) { - return errp.New("OP_INFO is not supposed to exist below v4.3.0") - } - - device.version = version - device.product = &product - } - return nil -} - // Init initializes the device. It changes the status to StatusRequireAppUpgrade if needed, // otherwise performs the attestation check, unlock, and noise pairing. This call is blocking. // After this call finishes, Status() will be either: @@ -241,11 +246,30 @@ func (device *Device) Init() error { device.channelHashDeviceVerified = false device.sendCipher = nil device.receiveCipher = nil - device.changeStatus(StatusConnected) - if err := device.inferVersionAndProduct(); err != nil { - return err + if device.version == nil || device.version.AtLeast(semver.NewSemVer(9, 2, 0)) { + deviceInfo, err := device.info() + if err != nil { + return errp.New( + "REQ_INFO unavailable; need to provide version and product via the USB HID descriptor") + } + device.log.Info(fmt.Sprintf("REQ_INFO: version=%s, product=%s", deviceInfo.version, + deviceInfo.product)) + device.version = deviceInfo.version + device.product = &deviceInfo.product + + if !deviceInfo.version.AtLeast(semver.NewSemVer(9, 20, 0)) { + device.changeStatus(StatusConnected) + } else if deviceInfo.unlocked { + device.changeStatus(StatusUnlocked) + } else if *deviceInfo.initialized { + // deviceInfo.initialized is not nil if version is at least 9.20.0. + device.changeStatus(StatusConnected) + } else { + device.changeStatus(StatusUninitialized) + } } + if device.version.AtLeast(lowestNonSupportedFirmwareVersion) { device.changeStatus(StatusRequireAppUpgrade) return nil diff --git a/api/firmware/mnemonic.go b/api/firmware/mnemonic.go index b96de4b..a3f52cc 100644 --- a/api/firmware/mnemonic.go +++ b/api/firmware/mnemonic.go @@ -38,7 +38,7 @@ func (device *Device) ShowMnemonic() error { return errp.New("unexpected response") } if device.status == StatusSeeded { - device.changeStatus(StatusInitialized) + device.changeStatus(StatusUnlocked) } return nil } @@ -63,7 +63,7 @@ func (device *Device) RestoreFromMnemonic() error { if !ok { return errp.New("unexpected response") } - device.changeStatus(StatusInitialized) + device.changeStatus(StatusUnlocked) return nil } diff --git a/api/firmware/pairing.go b/api/firmware/pairing.go index b99604c..1e814c9 100644 --- a/api/firmware/pairing.go +++ b/api/firmware/pairing.go @@ -175,7 +175,7 @@ func (device *Device) ChannelHashVerify(ok bool) { return } if info.Initialized { - device.changeStatus(StatusInitialized) + device.changeStatus(StatusUnlocked) } else { device.changeStatus(StatusUninitialized) } diff --git a/api/firmware/query.go b/api/firmware/query.go index 21b649f..50a2857 100644 --- a/api/firmware/query.go +++ b/api/firmware/query.go @@ -37,7 +37,7 @@ const ( hwwReqRetry = "\x01" // Cancel any outstanding request. // hwwReqCancel = "\x02" - // INFO api call (used to be OP_INFO api call), graduated to the toplevel framing so it works + // REQ_INFO api call (used to be OP_INFO api call), graduated to the toplevel framing so it works // the same way for all firmware versions. hwwInfo = "i" diff --git a/api/firmware/status.go b/api/firmware/status.go index a9fdb12..db8fce5 100644 --- a/api/firmware/status.go +++ b/api/firmware/status.go @@ -24,7 +24,7 @@ const ( StatusConnected Status = "connected" // StatusUnpaired means the pairing has not been confirmed yet. After the pairing screen has - // been confirmed, we move to StatusUninitialized or StatusInitialized depending on the device + // been confirmed, we move to StatusUninitialized or StatusUnlocked depending on the device // status. StatusUnpaired Status = "unpaired" @@ -36,12 +36,12 @@ const ( StatusUninitialized Status = "uninitialized" // StatusSeeded is after SetPassword(), before CreateBack() during initialization of the - // device. Use CreateBackup() to move to StatusInitialized. + // device. Use CreateBackup() to move to StatusUnlocked. StatusSeeded Status = "seeded" - // StatusInitialized means the device is seeded and the backup was created, and the device is + // StatusUnlocked means the device is seeded and the backup was created, and the device is // unlocked. The keystore is ready to use. - StatusInitialized Status = "initialized" + StatusUnlocked Status = "unlocked" // StatusRequireFirmwareUpgrade means that the a firmware upgrade is required before being able // to proceed to StatusLoggedIn or StatusSeeded (firmware version too old). diff --git a/api/firmware/system.go b/api/firmware/system.go index c290e15..1733207 100644 --- a/api/firmware/system.go +++ b/api/firmware/system.go @@ -87,7 +87,7 @@ func (device *Device) SetPassword(seedLen int) error { if seedLen == 16 && !device.version.AtLeast(semver.NewSemVer(9, 6, 0)) { return UnsupportedError("9.6.0") } - if device.status == StatusInitialized { + if device.status == StatusUnlocked { return errp.New("invalid status") } request := &messages.Request{