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

Support vehicle data over BLE #330

Merged
merged 2 commits into from
Nov 18, 2024
Merged
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
49 changes: 49 additions & 0 deletions cmd/tesla-control/commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,35 @@ type Command struct {
domain protocol.Domain
}

var categoriesByName = map[string]vehicle.StateCategory{
"charge": vehicle.StateCategoryCharge,
"climate": vehicle.StateCategoryClimate,
"drive": vehicle.StateCategoryDrive,
"closures": vehicle.StateCategoryClosures,
"charge-schedule": vehicle.StateCategoryChargeSchedule,
"precondition-schedule": vehicle.StateCategoryPreconditioningSchedule,
"tire-pressure": vehicle.StateCategoryTirePressure,
"media": vehicle.StateCategoryMedia,
"media-detail": vehicle.StateCategoryMediaDetail,
"software-update": vehicle.StateCategorySoftwareUpdate,
"parental-controls": vehicle.StateCategoryParentalControls,
}

func categoryNames() []string {
var names []string
for name, _ := range categoriesByName {
names = append(names, name)
}
return names
}

func GetCategory(nameStr string) (vehicle.StateCategory, error) {
if category, ok := categoriesByName[strings.ToLower(nameStr)]; ok {
return category, nil
}
return 0, fmt.Errorf("unrecognized state category '%s'", nameStr)
}

func GetDegree(degStr string) (float32, error) {
deg, err := strconv.ParseFloat(degStr, 32)
if err != nil {
Expand Down Expand Up @@ -1135,4 +1164,24 @@ var commands = map[string]*Command{
return car.BatchRemovePreconditionSchedules(ctx, home, work, other)
},
},
"state": {
help: "Fetch vehicle state over BLE.",
requiresAuth: true,
requiresFleetAPI: false,
args: []Argument{
Argument{name: "CATEGORY", help: "One of " + strings.Join(categoryNames(), ", ")},
},
handler: func(ctx context.Context, acct *account.Account, car *vehicle.Vehicle, args map[string]string) error {
category, err := GetCategory(args["CATEGORY"])
if err != nil {
return err
}
data, err := car.GetState(ctx, category)
if err != nil {
return err
}
fmt.Println(protojson.Format(data))
return nil
},
},
}
2 changes: 0 additions & 2 deletions internal/authentication/jwt_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import (
"github.com/golang-jwt/jwt/v5"
)

const testVIN = "0123456789abcdefX"

func TestVerify(t *testing.T) {
pkey := []byte{
0x04, 0x77, 0x5e, 0x2e, 0xf5, 0x70, 0xd2, 0x92, 0xdf, 0x42, 0x4c, 0x09,
Expand Down
37 changes: 37 additions & 0 deletions internal/authentication/peer.go
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,40 @@ func (p *Peer) hmacTag(message *universal.RoutableMessage, hmacData *signatures.
}
return meta.Checksum(message.GetProtobufMessageAsBytes()), nil
}

func RequestID(message *universal.RoutableMessage) []byte {
sigData := message.GetSignatureData()
if sigData.GetSigType() == nil {
return nil
}

switch s := sigData.GetSigType().(type) {
case *signatures.SignatureData_AES_GCM_PersonalizedData:
return append(
[]byte{byte(signatures.SignatureType_SIGNATURE_TYPE_AES_GCM_PERSONALIZED)},
s.AES_GCM_PersonalizedData.GetTag()...)
case *signatures.SignatureData_HMAC_PersonalizedData:
tag := s.HMAC_PersonalizedData.GetTag()
if message.GetToDestination().GetDomain() == universal.Domain_DOMAIN_VEHICLE_SECURITY {
tag = tag[:16]
}
return append(
[]byte{byte(signatures.SignatureType_SIGNATURE_TYPE_HMAC_PERSONALIZED)}, tag...)
default:
return nil
}
}

func (p *Peer) responseMetadata(message *universal.RoutableMessage, id []byte, counter uint32) ([]byte, error) {
meta := newMetadata()
meta.Add(signatures.Tag_TAG_SIGNATURE_TYPE, []byte{byte(signatures.SignatureType_SIGNATURE_TYPE_AES_GCM_RESPONSE)})
meta.Add(signatures.Tag_TAG_DOMAIN, []byte{byte(message.GetFromDestination().GetDomain())})
if err := meta.Add(signatures.Tag_TAG_PERSONALIZATION, p.verifierName); err != nil {
return nil, err
}
meta.AddUint32(signatures.Tag_TAG_COUNTER, counter)
meta.AddUint32(signatures.Tag_TAG_FLAGS, message.Flags)
meta.Add(signatures.Tag_TAG_REQUEST_HASH, id)
meta.AddUint32(signatures.Tag_TAG_FAULT, uint32(message.GetSignedMessageStatus().GetSignedMessageFault()))
return meta.Checksum(nil), nil
}
46 changes: 46 additions & 0 deletions internal/authentication/peer_test.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package authentication

import (
"bytes"
"testing"

"github.com/teslamotors/vehicle-command/pkg/protocol/protobuf/signatures"
universal "github.com/teslamotors/vehicle-command/pkg/protocol/protobuf/universalmessage"
)

Expand Down Expand Up @@ -84,3 +86,47 @@ func checkError(t *testing.T, err error, expectedCode universal.MessageFault_E)
t.Errorf("Got unexpected error type: %s", err)
}
}

func TestRequestID(t *testing.T) {
tag := []byte{0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18}
message := universal.RoutableMessage{
ToDestination: &universal.Destination{
SubDestination: &universal.Destination_Domain{
Domain: universal.Domain_DOMAIN_VEHICLE_SECURITY,
},
},
SubSigData: &universal.RoutableMessage_SignatureData{
SignatureData: &signatures.SignatureData{
SigType: &signatures.SignatureData_HMAC_PersonalizedData{
HMAC_PersonalizedData: &signatures.HMAC_Personalized_Signature_Data{
Tag: append([]byte{}, tag...),
},
},
},
},
}
id := RequestID(&message)
if len(id) != 17 {
t.Errorf("Expected 17-byte id, but got %d bytes", len(id))
}
if id[0] != byte(signatures.SignatureType_SIGNATURE_TYPE_HMAC_PERSONALIZED) {
t.Errorf("Invalid first byte of request ID: %02x", id[0])
}
if !bytes.Equal(id[1:], tag[:16]) {
t.Errorf("Expected: %02x", tag[:16])
t.Errorf("Observed: %02x", id[1:])
}

message.ToDestination.SubDestination = &universal.Destination_Domain{
Domain: universal.Domain_DOMAIN_INFOTAINMENT,
}

id = RequestID(&message)
if id[0] != byte(signatures.SignatureType_SIGNATURE_TYPE_HMAC_PERSONALIZED) {
t.Errorf("Invalid first byte of request ID: %02x", id[0])
}
if !bytes.Equal(id[1:], tag) {
t.Errorf("Expected: %02x", tag)
t.Errorf("Observed: %02x", id[1:])
}
}
29 changes: 29 additions & 0 deletions internal/authentication/signer.go
Original file line number Diff line number Diff line change
Expand Up @@ -211,3 +211,32 @@ func (s *Signer) AuthorizeHMAC(message *universal.RoutableMessage, expiresIn tim
}
return nil
}

// Decrypt a Verifier message in place.
//
// Returns the anti-replay counter included in the message, which the client must verify increases
// monotonically for a given id or is inside of a sliding window.
func (s *Signer) Decrypt(message *universal.RoutableMessage, id []byte) (uint32, error) {
gcmInfo := message.GetSignatureData().GetAES_GCM_ResponseData()
if gcmInfo == nil {
return 0, newError(errCodeBadParameter, "missing AES-GCM data")
}
authenticatedData, err := s.responseMetadata(message, id, gcmInfo.Counter)
if err != nil {
return 0, nil
}
plaintext, err := s.session.Decrypt(
gcmInfo.Nonce,
message.GetProtobufMessageAsBytes(),
authenticatedData,
gcmInfo.Tag,
)
if err != nil {
return 0, err
}
message.Payload = &universal.RoutableMessage_ProtobufMessageAsBytes{
ProtobufMessageAsBytes: plaintext,
}
message.SubSigData = nil
return gcmInfo.Counter, nil
}
88 changes: 39 additions & 49 deletions internal/authentication/verifier.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ type Verifier struct {
Peer
lock sync.Mutex
window uint64
handle uint32
}

// NewVerifier returns a Verifier.
Expand Down Expand Up @@ -72,6 +73,12 @@ func (v *Verifier) rotateEpochIfNeeded(force bool) error {
return nil
}

func (v *Verifier) AssignHandle(handle uint32) {
v.lock.Lock()
v.handle = handle
v.lock.Unlock()
}

func (v *Verifier) sessionInfo() (*signatures.SessionInfo, error) {
if err := v.adjustClock(); err != nil {
return nil, err
Expand All @@ -81,6 +88,7 @@ func (v *Verifier) sessionInfo() (*signatures.SessionInfo, error) {
PublicKey: v.session.LocalPublicBytes(),
Epoch: v.epoch[:],
ClockTime: v.timestamp(),
Handle: v.handle,
}
return info, nil
}
Expand Down Expand Up @@ -217,55 +225,6 @@ func (v *Verifier) Verify(message *universal.RoutableMessage) (plaintext []byte,
return
}

// updateSlidingWindow takes the current counter value (i.e., the highest
// counter value of any authentic message received so far), the current sliding
// window, and the newCounter value from an incoming message. The function
// returns the updated counter and window values and sets ok to true if it
// could confirm that newCounter has never been previously used. If ok is
// false, then updatedCounter = counter and updatedWindow = window.
func updateSlidingWindow(counter uint32, window uint64, newCounter uint32) (updatedCounter uint32, updatedWindow uint64, ok bool) {
// If we exit early due to an error, we want to leave the counter/window
// state unchanged. Therefore we initialize return values to the current
// state.
updatedCounter = counter
updatedWindow = window
ok = false

if counter == newCounter {
// This counter value has been used before.
return
}

if newCounter < counter {
// This message arrived out of order.
age := counter - newCounter
if age > windowSize {
// Our history doesn't go back this far, so we can't determine if
// we've seen this newCounter value before.
return
}
if window>>(age-1)&1 == 1 {
// The newCounter value has been used before.
return
}
// Everything looks good.
ok = true
updatedWindow |= (1 << (age - 1))
return
}

// If we've reached this point, newCounter > counter, so newCounter is valid.
ok = true
updatedCounter = newCounter
// Compute how far we need to shift our sliding window.
shiftCount := newCounter - counter
updatedWindow <<= shiftCount
// We need to set the bit in our window that corresponds to counter (if
// newCounter = counter + 1, then this is the first [LSB] of the window).
updatedWindow |= uint64(1) << (shiftCount - 1)
return
}

func (v *Verifier) verifyGCM(message *universal.RoutableMessage, gcmData *signatures.AES_GCM_Personalized_Signature_Data) (plaintext []byte, err error) {
if err = v.verifySessionInfo(message, gcmData); err != nil {
return nil, err
Expand Down Expand Up @@ -337,3 +296,34 @@ func (v *Verifier) verifySessionInfo(message *universal.RoutableMessage, info se
}
return nil
}

// Encrypt a message response in place.
//
// The message id must uniquely identify the Signer's request that prompted the message. The
// counter must increase monotonically for a given id.
func (v *Verifier) Encrypt(message *universal.RoutableMessage, id []byte, counter uint32) error {
plaintext := message.GetProtobufMessageAsBytes()
authenticatedData, err := v.responseMetadata(message, id, counter)
if err != nil {
return err
}
nonce, ciphertext, tag, err := v.session.Encrypt(plaintext, authenticatedData)
if err != nil {
return err
}
message.SubSigData = &universal.RoutableMessage_SignatureData{
SignatureData: &signatures.SignatureData{
SigType: &signatures.SignatureData_AES_GCM_ResponseData{
AES_GCM_ResponseData: &signatures.AES_GCM_Response_Signature_Data{
Counter: counter,
Nonce: nonce,
Tag: tag,
},
},
},
}
message.Payload = &universal.RoutableMessage_ProtobufMessageAsBytes{
ProtobufMessageAsBytes: ciphertext,
}
return nil
}
Loading
Loading