Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

api/firmware/device: info use device status fields #98

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions api/firmware/backup.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
2 changes: 1 addition & 1 deletion api/firmware/backup_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
106 changes: 65 additions & 41 deletions api/firmware/device.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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:]
Expand All @@ -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.
Expand All @@ -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:
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions api/firmware/mnemonic.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand All @@ -63,7 +63,7 @@ func (device *Device) RestoreFromMnemonic() error {
if !ok {
return errp.New("unexpected response")
}
device.changeStatus(StatusInitialized)
device.changeStatus(StatusUnlocked)
return nil
}

Expand Down
2 changes: 1 addition & 1 deletion api/firmware/pairing.go
Original file line number Diff line number Diff line change
Expand Up @@ -175,7 +175,7 @@ func (device *Device) ChannelHashVerify(ok bool) {
return
}
if info.Initialized {
device.changeStatus(StatusInitialized)
device.changeStatus(StatusUnlocked)
} else {
device.changeStatus(StatusUninitialized)
}
Expand Down
2 changes: 1 addition & 1 deletion api/firmware/query.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
8 changes: 4 additions & 4 deletions api/firmware/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand All @@ -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).
Expand Down
2 changes: 1 addition & 1 deletion api/firmware/system.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{
Expand Down