mirror of
https://github.com/tobiasehlert/teslamateapi.git
synced 2026-02-27 09:54:18 +08:00
supporting TeslaMate encryption of API tokens (#141)
* updating sql of getting data from psql * adding decrypt of token Co-authored-by: Leland Sindt <leland@sway.org>
This commit is contained in:
@@ -0,0 +1,94 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/sha256"
|
||||
"log"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// decryptAccessToken funct to decrypt tokens from database
|
||||
func decryptAccessToken(data string, encryptionKey string) string {
|
||||
|
||||
/*
|
||||
From Adrian....
|
||||
I had a look at how to decode the binary input without additional libraries. Below is sample code for Elixir. An important detail is that "Additional Authenticated Data (AAD) " is required to decrypt the tokens. The AAD is a fixed string, in this case "AES256GCM”.
|
||||
<< _type::bytes-1, length::integer, _tag::bytes-size(length), iv::bytes-12, ciphertag::bytes-16, ciphertext::bytes >> = input
|
||||
key = :crypto.hash(:sha256, key)
|
||||
aad = "AES256GCM"
|
||||
plaintext = :crypto.crypto_one_time_aead(:aes_256_gcm, key, iv, ciphertext, aad, ciphertag, false)
|
||||
|
||||
How the encrypted content looks like....
|
||||
+----------------------------------------------------------+----------------------+
|
||||
| HEADER | BODY |
|
||||
+-------------------+---------------+----------------------+----------------------+
|
||||
| Key Tag (n bytes) | IV (n bytes) | Ciphertag (16 bytes) | Ciphertext (n bytes) |
|
||||
+-------------------+---------------+----------------------+----------------------+
|
||||
| |_________________________________
|
||||
| |
|
||||
+---------------+-----------------+-------------------+
|
||||
| Type (1 byte) | Length (1 byte) | Key Tag (n bytes) |
|
||||
+---------------+-----------------+-------------------+
|
||||
*/
|
||||
|
||||
h := sha256.New()
|
||||
h.Write([]byte(encryptionKey))
|
||||
if gin.IsDebugging() {
|
||||
log.Printf("[debug] decryptAccessToken - Key: %x \n", h.Sum(nil))
|
||||
}
|
||||
|
||||
key := h.Sum(nil)
|
||||
block, err := aes.NewCipher(key)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
// first byte
|
||||
keyType := int([]rune(data)[0])
|
||||
// second byte
|
||||
keyLen := int([]rune(data)[1])
|
||||
keyTag := data[2 : 2+keyLen]
|
||||
if gin.IsDebugging() {
|
||||
log.Printf("[debug] decryptAccessToken - Type: %d \n", keyType)
|
||||
log.Printf("[debug] decryptAccessToken - Length: %d \n", keyLen)
|
||||
log.Printf("[debug] decryptAccessToken - Key Tag: %s \n", keyTag)
|
||||
}
|
||||
|
||||
/*
|
||||
With AES.GCM, 12-byte IV length is necessary for interoperability reasons.
|
||||
See https://github.com/danielberkompas/cloak/issues/93
|
||||
IV and nonce are often used interchangeably. Essentially though, an IV is a nonce with an additional requirement: it must be selected in a non-predictable way
|
||||
https://medium.com/@fridakahsas/salt-nonces-and-ivs-whats-the-difference-d7a44724a447#:~:text=IV%20and%20nonce%20are%20often,an%20IV%20must%20be%20random.
|
||||
*/
|
||||
|
||||
nonce := data[2+keyLen : 2+keyLen+12]
|
||||
if gin.IsDebugging() {
|
||||
log.Printf("[debug] decryptAccessToken - IV (hex): %x \n", nonce)
|
||||
|
||||
ciphertag := data[2+keyLen+12 : 2+keyLen+12+16]
|
||||
log.Printf("[debug] decryptAccessToken - Ciphertag (hex): %x \n", ciphertag)
|
||||
}
|
||||
|
||||
aesgcm, err := cipher.NewGCMWithTagSize(block, 16)
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
// https://stackoverflow.com/a/68353192
|
||||
// golang aes expects cipertag to append ciphertext....
|
||||
ciphertextTag := data[2+keyLen+12+16:] + data[2+keyLen+12:2+keyLen+12+16]
|
||||
|
||||
// AES256GCM -- Additional Authenticated Data (AAD)
|
||||
plaintext, err := aesgcm.Open(nil, []byte(nonce), []byte(ciphertextTag), []byte("AES256GCM"))
|
||||
if err != nil {
|
||||
panic(err.Error())
|
||||
}
|
||||
|
||||
if gin.IsDebugging() {
|
||||
// fmt.Printf("[debug] decryptAccessToken - Decrypted: %s\n", plaintext)
|
||||
}
|
||||
|
||||
return string(plaintext)
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
@@ -16,6 +17,7 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) {
|
||||
|
||||
// creating required vars
|
||||
var (
|
||||
CarsCommandsError1 = "Unable to load cars."
|
||||
TeslaAccessToken, TeslaVehicleID string
|
||||
jsonData map[string]interface{}
|
||||
err error
|
||||
@@ -78,33 +80,36 @@ func TeslaMateAPICarsCommandV1(c *gin.Context) {
|
||||
FROM cars
|
||||
WHERE id = $1
|
||||
LIMIT 1;`
|
||||
rows, err := db.Query(query, CarID)
|
||||
row := db.QueryRow(query, CarID)
|
||||
|
||||
// checking for errors in query
|
||||
if err != nil {
|
||||
TeslaMateAPIHandleErrorResponse(c, "TeslaMateAPICarsCommandV1", "Unable to load cars.", err.Error())
|
||||
err = row.Scan(
|
||||
&TeslaVehicleID,
|
||||
&TeslaAccessToken,
|
||||
)
|
||||
|
||||
switch err {
|
||||
case sql.ErrNoRows:
|
||||
TeslaMateAPIHandleErrorResponse(c, "TeslaMateAPICarsCommandV1", "No rows were returned!", err.Error())
|
||||
return
|
||||
case nil:
|
||||
// nothing wrong.. continuing
|
||||
break
|
||||
default:
|
||||
TeslaMateAPIHandleErrorResponse(c, "TeslaMateAPICarsCommandV1", CarsCommandsError1, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// defer closing rows
|
||||
defer rows.Close()
|
||||
|
||||
// looping through all results (even if it's only one..)
|
||||
for rows.Next() {
|
||||
// scanning row and putting values into the drive
|
||||
err = rows.Scan(
|
||||
&TeslaVehicleID,
|
||||
&TeslaAccessToken,
|
||||
)
|
||||
}
|
||||
|
||||
// checking for errors in query when doing scan action
|
||||
if err != nil {
|
||||
log.Println("[error] TeslaMateAPICarsCommandV1 error in sql query:", err)
|
||||
TeslaMateAPIHandleOtherResponse(c, http.StatusInternalServerError, "TeslaMateAPICarsCommandV1", gin.H{"error": "internal sql query error"})
|
||||
// load ENCRYPTION_KEY environment variable
|
||||
teslaMateEncryptionKey := getEnv("ENCRYPTION_KEY", "")
|
||||
if teslaMateEncryptionKey == "" {
|
||||
log.Println("[error] TeslaMateAPICarsCommandV1 can't get ENCRYPTION_KEY.. will fail to perform command.")
|
||||
TeslaMateAPIHandleOtherResponse(c, http.StatusInternalServerError, "TeslaMateAPICarsCommandV1", gin.H{"error": "missing ENCRYPTION_KEY env variable"})
|
||||
return
|
||||
}
|
||||
|
||||
// decrypt access token
|
||||
TeslaAccessToken = decryptAccessToken(TeslaAccessToken, teslaMateEncryptionKey)
|
||||
|
||||
client := &http.Client{}
|
||||
req, _ := http.NewRequest(http.MethodPost, "https://owner-api.teslamotors.com/api/1/vehicles/"+TeslaVehicleID+command, strings.NewReader(string(reqBody)))
|
||||
req.Header.Set("Authorization", "Bearer "+TeslaAccessToken)
|
||||
|
||||
Reference in New Issue
Block a user