SAIC: simplify (#27130)

This commit is contained in:
andig 2026-02-03 23:00:28 +01:00 • committed by GitHub
parent 26f79c4be5
commit 64b415c36d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 282 additions and 366 deletions

View file

@ -11,7 +11,6 @@ import (
"github.com/evcc-io/evcc/util/request"
"github.com/evcc-io/evcc/util/transport"
"github.com/evcc-io/evcc/vehicle/saic/requests"
"golang.org/x/oauth2"
)
const (
@ -56,19 +55,13 @@ func NewAPI(log *util.Logger, identity *Identity) *API {
}
func (v *API) doRepeatedRequest(path string, event_id string) error {
var req *http.Request
answer := requests.Answer{
Data: &v.request.Result,
}
token, err := v.identity.Token()
if err != nil {
v.request.Status = StatInvalid
return err
}
req, err = requests.CreateRequest(
req, err := requests.CreateRequest(
v.identity.baseUrl,
path,
http.MethodGet,
@ -81,9 +74,13 @@ func (v *API) doRepeatedRequest(path string, event_id string) error {
return err
}
_, err = v.DoRequest(req, &answer)
var res requests.Answer[requests.ChargeStatus]
_, err = doRequest(v, req, &res)
if err == nil {
v.request.Status = StatValid
v.request = ConcurrentRequest{
Status: StatValid,
Result: res.Data,
}
} else if err != api.ErrMustRetry {
v.request.Status = StatInvalid
}
@ -92,11 +89,10 @@ func (v *API) doRepeatedRequest(path string, event_id string) error {
// This is running concurrently
func (v *API) repeatRequest(path string, event_id string) {
var err error
var count int
v.request.Status = StatRunning
for err = api.ErrMustRetry; err == api.ErrMustRetry && count < 20; {
for err := api.ErrMustRetry; err == api.ErrMustRetry && count < 20; {
time.Sleep(2 * time.Second)
v.log.TRACE.Printf("Starting repeated query. Count: %d", count)
err = v.doRepeatedRequest(path, event_id)
@ -108,20 +104,17 @@ func (v *API) repeatRequest(path string, event_id string) {
if v.request.Status == StatRunning {
v.request.Status = StatInvalid
}
v.log.TRACE.Printf("Exiting repeated query. Count: %d", count)
}
func (v *API) DoRequest(req *http.Request, result *requests.Answer) (string, error) {
var body []byte
func doRequest[T any](v *API, req *http.Request, result *requests.Answer[T]) (string, error) {
resp, err := v.Do(req)
if err != nil {
return "", err
}
defer resp.Body.Close()
if resp.StatusCode == http.StatusUnauthorized {
v.log.TRACE.Printf("DoRequest: %s", resp.Status)
v.log.TRACE.Printf("doRequest: %s", resp.Status)
v.identity.Login()
return "", api.ErrMustRetry
}
@ -129,17 +122,17 @@ func (v *API) DoRequest(req *http.Request, result *requests.Answer) (string, err
event_id := resp.Header.Get("event-id")
if result != nil {
body, err = requests.DecryptAnswer(resp)
if err == nil {
if err2 := json.Unmarshal(body, result); err2 == nil && result.Code != 0 {
if result.Code == 4 {
err = api.ErrMustRetry
} else {
err = fmt.Errorf("%d: %s", result.Code, result.Message)
}
body, err2 := requests.DecodeResponse(resp)
if err2 != nil {
return event_id, fmt.Errorf("decrypt: %w", err2)
}
if err2 := json.Unmarshal(body, result); err2 == nil && result.Code != 0 {
if result.Code == 4 {
err = api.ErrMustRetry
} else {
err = fmt.Errorf("%d: %s", result.Code, result.Message)
}
} else {
err = fmt.Errorf("decrypt: %w", err)
}
}
@ -156,17 +149,13 @@ func (v *API) Vehicles() ([]Vehicle, error) {
*/
func (v *API) Wakeup(vin string) error {
var req *http.Request
var err error
var token *oauth2.Token
token, err = v.identity.Token()
token, err := v.identity.Token()
if err != nil {
return err
}
path := "vehicle/status?vin=" + requests.Sha256(vin)
req, err = requests.CreateRequest(
req, err := requests.CreateRequest(
v.identity.baseUrl,
path,
http.MethodGet,
@ -178,21 +167,14 @@ func (v *API) Wakeup(vin string) error {
return err
}
v.DoRequest(req, nil)
doRequest[any](v, req, nil)
return nil
}
// Status implements the /user/vehicles/<vin>/status api
func (v *API) Status(vin string) (requests.ChargeStatus, error) {
var req *http.Request
var res requests.ChargeStatus
var event_id string
var err error
var token *oauth2.Token
answer := requests.Answer{
Data: &res,
}
var zero requests.ChargeStatus
// Check if we are already running in the background
if v.request.Status == StatValid {
@ -203,18 +185,18 @@ func (v *API) Status(vin string) (requests.ChargeStatus, error) {
}
if v.request.Status == StatRunning {
v.log.TRACE.Printf("StatRunning. Exiting")
return res, api.ErrMustRetry
return zero, api.ErrMustRetry
}
v.log.TRACE.Printf("StatInvalid. Starting query")
token, err = v.identity.Token()
token, err := v.identity.Token()
if err != nil {
return res, err
return zero, err
}
path := "vehicle/charging/mgmtData?vin=" + requests.Sha256(vin)
// get charging status of vehicle
req, err = requests.CreateRequest(
req, err := requests.CreateRequest(
v.identity.baseUrl,
path,
http.MethodGet,
@ -223,18 +205,18 @@ func (v *API) Status(vin string) (requests.ChargeStatus, error) {
token.AccessToken,
"")
if err != nil {
return res, err
return zero, err
}
event_id, err = v.DoRequest(req, &answer)
var res requests.Answer[requests.ChargeStatus]
event_id, err := doRequest(v, req, &res)
if err != nil {
v.log.TRACE.Printf("Getting event id failed")
return res, err
return zero, err
}
if event_id == "" {
v.log.TRACE.Printf("Answer without event ID")
return res, api.ErrMustRetry
return zero, api.ErrMustRetry
}
req, err = requests.CreateRequest(
@ -246,20 +228,17 @@ func (v *API) Status(vin string) (requests.ChargeStatus, error) {
token.AccessToken,
event_id)
if err != nil {
v.log.TRACE.Printf("Could not create request %s", err.Error())
return res, err
return zero, err
}
_, err = v.DoRequest(req, &answer)
_, err = doRequest(v, req, &res)
// Continue checking....
if err == api.ErrMustRetry {
v.request.Status = StatRunning
v.log.TRACE.Printf(" No answer yet. Continue status query in background")
go v.repeatRequest(path, event_id)
} else if err != nil {
v.log.TRACE.Printf("doRequest failed with %s", err.Error())
}
return res, err
return res.Data, err
}

View file

@ -62,11 +62,6 @@ func (v *Identity) Login() error {
}
func (v *Identity) retrieveToken(data url.Values) (*oauth2.Token, error) {
var loginData requests.LoginData
answer := requests.Answer{
Data: &loginData,
}
data.Set("deviceId", v.deviceId)
data.Set("response_type", "code")
data.Set("scope", "all")
@ -92,24 +87,27 @@ func (v *Identity) retrieveToken(data url.Values) (*oauth2.Token, error) {
return nil, err
}
var body []byte
body, err = requests.DecryptAnswer(resp)
if err == nil && len(body) != 0 {
err = json.Unmarshal(body, &answer)
if err == nil && answer.Code != 0 {
err = fmt.Errorf("%d: %s", answer.Code, answer.Message)
}
tok := oauth2.Token{
AccessToken: loginData.Access_token,
RefreshToken: loginData.Refresh_token,
TokenType: loginData.Token_type,
}
tok.Expiry = time.Now().Add(time.Second * time.Duration(loginData.Expires_in))
return &tok, err
body, err := requests.DecodeResponse(resp)
if err != nil {
return nil, err
}
return nil, err
var res requests.Answer[requests.LoginData]
if err := json.Unmarshal(body, &res); err != nil {
return nil, err
}
if res.Code != 0 {
return nil, fmt.Errorf("%d: %s", res.Code, res.Message)
}
tok := oauth2.Token{
AccessToken: res.Data.Access_token,
RefreshToken: res.Data.Refresh_token,
TokenType: res.Data.Token_type,
Expiry: time.Now().Add(time.Second * time.Duration(res.Data.Expires_in)),
}
return &tok, nil
}
func (v *Identity) refreshToken(token *oauth2.Token) (*oauth2.Token, error) {

View file

@ -106,7 +106,7 @@ func (v *Provider) Range() (int64, error) {
val := res.RvsChargeStatus.FuelRangeElec
if val < 10 {
// Ok, 0 would be possible, but it's more likely that it's an invalid answer.
return val, fmt.Errorf("invalid raw range value: %d: %w", val, api.ErrMustRetry)
return 0, api.ErrMustRetry
}
return val / 10, nil
}

View file

@ -1,8 +0,0 @@
package requests
const (
CONTENT_ENCRYPTED = "1"
PARAM_AUTHENTICATION = "Basic c3dvcmQ6c3dvcmRfc2VjcmV0"
TENANT_ID = "459771"
USER_TYPE = "app"
)

View file

@ -1,60 +0,0 @@
package requests
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/hex"
)
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
func PKCS5Trimming(encrypt []byte) []byte {
padding := encrypt[len(encrypt)-1]
return encrypt[:len(encrypt)-int(padding)]
}
func Decrypt(cipherText, hexKey, hexIV string) string {
if len(cipherText) == 0 || len(hexKey) == 0 || len(hexIV) == 0 {
return ""
}
secretKey, _ := hex.DecodeString(hexKey)
ivParameter, _ := hex.DecodeString(hexIV)
block, err := aes.NewCipher(secretKey)
if err != nil {
return ""
}
decrypter := cipher.NewCBCDecrypter(block, ivParameter)
text, _ := hex.DecodeString(cipherText)
decrypted := make([]byte, len(text))
decrypter.CryptBlocks(decrypted, text)
return string(PKCS5Trimming(decrypted))
}
func Encrypt(plainText, hexKey, hexIV string) string {
if len(plainText) == 0 || len(hexKey) == 0 || len(hexIV) == 0 {
return ""
}
secretKey, _ := hex.DecodeString(hexKey)
ivParameter, _ := hex.DecodeString(hexIV)
block, err := aes.NewCipher(secretKey)
if err != nil {
return ""
}
content := PKCS5Padding([]byte(plainText), block.BlockSize())
encrypter := cipher.NewCBCEncrypter(block, ivParameter)
ciphertext := make([]byte, len(content))
encrypter.CryptBlocks(ciphertext, content)
return hex.EncodeToString(ciphertext)
}

View file

@ -1,87 +1,60 @@
package requests
import (
"strconv"
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/hex"
)
func CalculateRequestVerification(
resourcePath string,
sendDate int64,
tenant,
contentType,
bodyEncrypted,
token string,
) string {
dateString := strconv.FormatInt(sendDate, 10)
str9 := resourcePath + tenant + token + USER_TYPE
a2 := Md5(str9)
str10 := dateString + CONTENT_ENCRYPTED + contentType
a3 := Md5(a2 + str10)
str11 := resourcePath + tenant + token + USER_TYPE + dateString + CONTENT_ENCRYPTED + contentType + bodyEncrypted
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
padding := blockSize - len(ciphertext)%blockSize
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
return append(ciphertext, padtext...)
}
a5 := Md5(a3 + dateString)
func PKCS5Trimming(encrypt []byte) []byte {
padding := encrypt[len(encrypt)-1]
return encrypt[:len(encrypt)-int(padding)]
}
if len(a5) != 0 && len(str11) != 0 {
return HmacSha256(a5, str11)
} else {
func Decrypt(cipherText, hexKey, hexIV string) string {
if len(cipherText) == 0 || len(hexKey) == 0 || len(hexIV) == 0 {
return ""
}
}
func DecryptResponse(timeStamp, contentType, cipherText string) string {
str4 := timeStamp + CONTENT_ENCRYPTED + contentType
a2 := Md5(str4)
hashedTimeStamp := Md5(timeStamp)
if len(cipherText) != 0 {
return Decrypt(cipherText, a2, hashedTimeStamp)
}
return ""
}
func CalculateResponseVerification(str, str2, str3 string) string {
str4 := str + CONTENT_ENCRYPTED + str2
a2 := Md5(str4)
str5 := str + CONTENT_ENCRYPTED + str2 + str3
a4 := Md5(a2 + str)
return HmacSha256(a4, str5)
}
func EncryptRequest(path string, time int64, tenant, token, body, contentType string) string {
sendDate := strconv.FormatInt(time, 10)
// tenant
resourcePath := ""
if len(path) != 0 {
resourcePath = "/" + path
secretKey, _ := hex.DecodeString(hexKey)
ivParameter, _ := hex.DecodeString(hexIV)
block, err := aes.NewCipher(secretKey)
if err != nil {
return ""
}
encryptedBody := ""
decrypter := cipher.NewCBCDecrypter(block, ivParameter)
text, _ := hex.DecodeString(cipherText)
decrypted := make([]byte, len(text))
decrypter.CryptBlocks(decrypted, text)
if len(body) != 0 {
sb3 := Md5(resourcePath+tenant+token+USER_TYPE) + sendDate + CONTENT_ENCRYPTED + contentType
a2 := Md5(sb3)
a3 := Md5(sendDate)
if len(body) != 0 && len(a2) != 0 && len(a3) != 0 {
encryptedBody = Encrypt(body, a2, a3)
}
}
return encryptedBody
return string(PKCS5Trimming(decrypted))
}
func DecryptRequest(path string, time int64, tenant, token, body, contentType string) string {
timeStamp := strconv.FormatInt(time, 10)
resourcePath := ""
if len(path) != 0 {
resourcePath = "/" + path
func Encrypt(plainText, hexKey, hexIV string) string {
if len(plainText) == 0 || len(hexKey) == 0 || len(hexIV) == 0 {
return ""
}
if len(body) != 0 {
sb3 := Md5(resourcePath+tenant+token+USER_TYPE) + timeStamp + CONTENT_ENCRYPTED + contentType
a2 := Md5(sb3)
timeStampHash := Md5(timeStamp)
return Decrypt(body, a2, timeStampHash)
secretKey, _ := hex.DecodeString(hexKey)
ivParameter, _ := hex.DecodeString(hexIV)
block, err := aes.NewCipher(secretKey)
if err != nil {
return ""
}
return ""
content := PKCS5Padding([]byte(plainText), block.BlockSize())
encrypter := cipher.NewCBCEncrypter(block, ivParameter)
ciphertext := make([]byte, len(content))
encrypter.CryptBlocks(ciphertext, content)
return hex.EncodeToString(ciphertext)
}

View file

@ -1,32 +0,0 @@
package requests
import (
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
)
func Md5(value string) string {
if len(value) == 0 {
return ""
}
result := md5.Sum([]byte(value))
return hex.EncodeToString(result[:])
}
func Sha1(value string) string {
if len(value) == 0 {
return ""
}
result := sha1.Sum([]byte(value))
return hex.EncodeToString(result[:])
}
func Sha256(value string) string {
if len(value) == 0 {
return ""
}
result := sha256.Sum256([]byte(value))
return hex.EncodeToString(result[:])
}

View file

@ -0,0 +1,40 @@
package requests
import (
"crypto/hmac"
"crypto/md5"
"crypto/sha1"
"crypto/sha256"
"encoding/hex"
"hash"
)
func sum(hash hash.Hash, value string) string {
if len(value) == 0 {
return ""
}
hash.Write([]byte(value))
return hex.EncodeToString(hash.Sum(nil))
}
func Md5(value string) string {
return sum(md5.New(), value)
}
func Sha1(value string) string {
return sum(sha1.New(), value)
}
func Sha256(value string) string {
return sum(sha256.New(), value)
}
func HmacSha256(secret string, message string) string {
if len(secret) == 0 || len(message) == 0 {
return ""
}
mac := hmac.New(sha256.New, []byte(secret))
mac.Write([]byte(message))
return hex.EncodeToString(mac.Sum(nil))
}

View file

@ -1,18 +0,0 @@
package requests
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
)
func HmacSha256(secret string, message string) string {
if len(secret) == 0 || len(message) == 0 {
return ""
}
key := []byte(secret)
h := hmac.New(sha256.New, key)
h.Write([]byte(message))
return hex.EncodeToString(h.Sum(nil))
}

View file

@ -0,0 +1,135 @@
package requests
import (
"bytes"
"errors"
"io"
"net/http"
"strconv"
"strings"
"time"
)
func Decorate(req *http.Request) error {
req.Header.Set("tenant-id", TENANT_ID)
req.Header.Set("user-type", USER_TYPE)
req.Header.Set("app-content-encrypted", CONTENT_ENCRYPTED)
req.Header.Set("Authorization", PARAM_AUTHENTICATION)
return nil
}
func encryptRequest(resourcePath string, sendDate int64, tenant, token, body, contentType string) string {
if len(body) == 0 {
return ""
}
dateString := strconv.FormatInt(sendDate, 10)
// tenant
if len(resourcePath) != 0 {
resourcePath = "/" + resourcePath
}
sb3 := Md5(resourcePath+tenant+token+USER_TYPE) + dateString + CONTENT_ENCRYPTED + contentType
a2 := Md5(sb3)
a3 := Md5(dateString)
return Encrypt(body, a2, a3)
}
func calculateRequestVerification(
resourcePath string,
sendDate int64,
tenant, contentType, bodyEncrypted, token string,
) string {
dateString := strconv.FormatInt(sendDate, 10)
str9 := resourcePath + tenant + token + USER_TYPE
a2 := Md5(str9)
str10 := dateString + CONTENT_ENCRYPTED + contentType
a3 := Md5(a2 + str10)
str11 := resourcePath + tenant + token + USER_TYPE + dateString + CONTENT_ENCRYPTED + contentType + bodyEncrypted
a5 := Md5(a3 + dateString)
return HmacSha256(a5, str11)
}
func CreateRequest(baseUrl, path, httpMethod, request, contentType, token, eventId string) (*http.Request, error) {
sendDate := time.Now().UnixMilli()
endpoint := baseUrl + path
if len(request) != 0 {
request = encryptRequest(
path,
sendDate,
TENANT_ID,
token,
request,
contentType)
}
req, err := http.NewRequest(httpMethod, endpoint, bytes.NewReader([]byte(request)))
if err != nil {
return nil, err
}
Decorate(req)
req.Header.Set("app-send-date", strconv.FormatInt(sendDate, 10))
req.Header.Set("original-content-type", contentType)
if len(token) != 0 {
req.Header.Set("blade-auth", token)
}
if len(eventId) != 0 {
req.Header.Set("event-id", eventId)
}
replace := strings.Replace(endpoint, baseUrl, "/", -1)
req.Header.Set("app-verification-string",
calculateRequestVerification(
replace,
sendDate,
TENANT_ID,
contentType,
request,
token))
return req, nil
}
func decryptResponse(timeStamp, contentType, cipherText string) string {
if len(cipherText) == 0 {
return ""
}
str4 := timeStamp + CONTENT_ENCRYPTED + contentType
a2 := Md5(str4)
hashedTimeStamp := Md5(timeStamp)
return Decrypt(cipherText, a2, hashedTimeStamp)
}
func DecodeResponse(resp *http.Response) ([]byte, error) {
if resp.StatusCode != http.StatusOK {
return nil, errors.New(resp.Status)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return body, err
}
if resp.Header.Get("app-content-encrypted") != "1" {
return body, nil
}
decoded := decryptResponse(
resp.Header.Get("app-send-date"),
resp.Header.Get("original-content-type"),
string(body))
return []byte(decoded), nil
}

View file

@ -1,98 +0,0 @@
package requests
import (
"bytes"
"errors"
"io"
"net/http"
"strconv"
"strings"
"time"
)
func Decorate(req *http.Request) error {
req.Header.Set("tenant-id", TENANT_ID)
req.Header.Set("user-type", USER_TYPE)
req.Header.Set("app-content-encrypted", CONTENT_ENCRYPTED)
req.Header.Set("Authorization", PARAM_AUTHENTICATION)
return nil
}
func CreateRequest(
baseUrl string,
path string,
httpMethod string,
request string,
contentType string,
token string,
eventId string,
) (*http.Request, error) {
appSendDate := time.Now().UnixMilli()
endpoint := baseUrl + path
if len(request) != 0 {
request = EncryptRequest(
path,
appSendDate,
TENANT_ID,
token,
request,
contentType)
}
bodyReader := bytes.NewReader([]byte(request))
req, err := http.NewRequest(httpMethod, endpoint, bodyReader)
if err != nil {
return nil, err
}
Decorate(req)
req.Header.Set("app-send-date", strconv.FormatInt(appSendDate, 10))
req.Header.Set("original-content-type", contentType)
if len(token) != 0 {
req.Header.Set("blade-auth", token)
}
if len(eventId) != 0 {
req.Header.Set("event-id", eventId)
}
replace := strings.Replace(endpoint, baseUrl, "/", -1)
req.Header.Set("app-verification-string",
CalculateRequestVerification(
replace,
appSendDate,
TENANT_ID,
contentType,
request,
token))
return req, nil
}
func DecryptAnswer(resp *http.Response) ([]byte, error) {
var body string
var bodyRaw []byte
var err error
if resp.StatusCode == http.StatusOK {
bodyRaw, err = io.ReadAll(resp.Body)
if err != nil {
return bodyRaw, err
}
if resp.Header.Get("app-content-encrypted") == "1" {
body = DecryptResponse(
resp.Header.Get("app-send-date"),
resp.Header.Get("original-content-type"),
string(bodyRaw))
} else {
body = string(bodyRaw)
}
} else {
err = errors.New(resp.Status)
}
return []byte(body), err
}

View file

@ -1,5 +1,12 @@
package requests
const (
CONTENT_ENCRYPTED = "1"
PARAM_AUTHENTICATION = "Basic c3dvcmQ6c3dvcmRfc2VjcmV0"
TENANT_ID = "459771"
USER_TYPE = "app"
)
type ChargeStatus struct {
RvsChargeStatus struct {
MileageSinceLastCharge int
@ -91,8 +98,8 @@ type LoginData struct {
Jti string
}
type Answer struct {
type Answer[T any] struct {
Code int `json:"code"`
Data any `json:"data,omitempty"`
Message string `json:"message"`
Data T `json:"data,omitempty"`
}