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:
Tobias Lindberg
2022-05-08 12:29:25 +02:00
committed by GitHub
parent 92f9abef16
commit 3f9e8c471b
2 changed files with 119 additions and 20 deletions
+94
View File
@@ -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)
}
+25 -20
View File
@@ -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)