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{