Anker Solix: the encrypted transport the app uses, ported from upstream v3.8.2
anker-solix-api v3.8.2 (2026-09-13) worked out the mobile app's REST payload encryption end to end — a second ECDH handshake after login, then every body AES-sealed and HMAC-signed both ways. Anker's servers still accept the plain JSON this connector has always sent, and upstream leaves the mode off for that reason; this ports it so that the day they stop is a config change rather than an outage. payloadcrypto.go holds the handshake (openapi/oauth/key/exchange, both public keys wrapped under the app's presetKey) and the per-request envelope (AES-128-CBC under secret[:16], x-signature keyed on its ascii hex). The handshake lives beside the token in the shared session and is dropped with it, so a fresh login is a fresh handshake before the retry. The switch is a global-layer field, payloadEncryption, default off: it is how the connector talks to Anker rather than whose account it talks as, so it does not cascade — ankerCloudConfig copies it into every per-user config, and the six places that built that map by hand now go through it. Verified against a fake cloud that implements the server half from upstream's description, not against Anker's; the switch should be tried on a test account before it is relied on. Nothing else in v3.8.2 reaches an EV charger: the Solarbank expansion sizing, the Gen 2 smart meter and the new site parameter types are other devices, and the A5191 MQTT block is unchanged. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a26c608fc3
commit
f8b39fa0a3
9 files changed
+733
-63
No files matched your search
@@ -181,6 +181,10 @@ type ankerResolution struct {
|
||||
orgEnabled bool // org gate (default true; gates the org's users)
|
||||
enabled bool // caller's personal enable flag
|
||||
|
||||
// payloadEncryption is the global layer's switch for the plugin's REST
|
||||
// transport (off | on), copied into every cloud config as-is.
|
||||
payloadEncryption string
|
||||
|
||||
// hiddenForOrg is what the global layer hides from everyone below it;
|
||||
// hiddenForUser adds what the caller's organization hides from its own users.
|
||||
// They gate both the pickers the clients draw and which layer's stored mode
|
||||
@@ -225,6 +229,8 @@ func (s *Server) resolveAnker(ctx context.Context, who *callerIdentity, userRaw
|
||||
available: masterEnabled,
|
||||
orgEnabled: !oStored.Disabled, // default true; off only when the org disabled it
|
||||
enabled: uStored.Enabled,
|
||||
|
||||
payloadEncryption: g["payloadEncryption"],
|
||||
}
|
||||
|
||||
// What each layer below may still be offered. The global list reaches
|
||||
@@ -621,11 +627,7 @@ func (s *Server) handleAnkerHealth(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := map[string]string{
|
||||
"email": res.eff.Email,
|
||||
"password": res.eff.Password,
|
||||
"country": res.eff.Country,
|
||||
}
|
||||
cfg := ankerCloudConfig(res)
|
||||
h, err := s.plugins.HealthCheckWith(r.Context(), ankerPlugin, cfg)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
|
||||
@@ -653,11 +655,7 @@ func (s *Server) handleAnkerChargers(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
cfg := map[string]string{
|
||||
"email": res.eff.Email,
|
||||
"password": res.eff.Password,
|
||||
"country": res.eff.Country,
|
||||
}
|
||||
cfg := ankerCloudConfig(res)
|
||||
raw, err := s.plugins.InvokeWith(r.Context(), ankerPlugin, cfg, "chargers", nil)
|
||||
if err != nil {
|
||||
writeJSON(w, http.StatusBadGateway, map[string]any{"error": err.Error()})
|
||||
@@ -694,11 +692,7 @@ func (s *Server) handleAnkerChargerDetails(w http.ResponseWriter, r *http.Reques
|
||||
return
|
||||
}
|
||||
|
||||
cfg := map[string]string{
|
||||
"email": res.eff.Email,
|
||||
"password": res.eff.Password,
|
||||
"country": res.eff.Country,
|
||||
}
|
||||
cfg := ankerCloudConfig(res)
|
||||
raw, err := s.plugins.InvokeWith(r.Context(), ankerPlugin, cfg, "charger-details",
|
||||
mustJSON(map[string]any{"sn": sn}))
|
||||
if err != nil {
|
||||
@@ -813,11 +807,7 @@ func (s *Server) ankerCardGate(w http.ResponseWriter, r *http.Request) (*callerI
|
||||
writeError(w, http.StatusTooManyRequests, "too many card requests; please slow down")
|
||||
return nil, "", nil, false
|
||||
}
|
||||
return who, sn, map[string]string{
|
||||
"email": res.eff.Email,
|
||||
"password": res.eff.Password,
|
||||
"country": res.eff.Country,
|
||||
}, true
|
||||
return who, sn, ankerCloudConfig(res), true
|
||||
}
|
||||
|
||||
// ankerCardWrite runs one card capability and relays its document. The audit
|
||||
|
||||
Reference in new issue
Block a user