680 lines
18 KiB
Go
680 lines
18 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/aes"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"os/signal"
|
|
"strings"
|
|
"syscall"
|
|
"time"
|
|
|
|
"inou/lib"
|
|
)
|
|
|
|
const (
|
|
baseURL = "https://cloud.renpho.com"
|
|
aesKey = "ed*wijdi$h6fe3ew"
|
|
)
|
|
|
|
// Renpho API response
|
|
type apiResponse struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg"`
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
|
|
// Login response
|
|
type loginUser struct {
|
|
UserID json.Number `json:"id"`
|
|
Token string `json:"token"`
|
|
Email string `json:"email"`
|
|
}
|
|
|
|
// Table mapping
|
|
type tableMapping struct {
|
|
UserIDs []json.Number `json:"userIds"`
|
|
TableName string `json:"tableName"`
|
|
Count int `json:"count"`
|
|
}
|
|
|
|
// Measurement from Renpho
|
|
type measurement struct {
|
|
TimeStamp int64 `json:"timeStamp"`
|
|
Weight float64 `json:"weight"`
|
|
BodyFat float64 `json:"bodyfat"`
|
|
Water float64 `json:"water"`
|
|
BMR float64 `json:"bmr"`
|
|
BodyAge float64 `json:"bodyage"`
|
|
Muscle float64 `json:"muscle"`
|
|
Bone float64 `json:"bone"`
|
|
SubFat float64 `json:"subfat"`
|
|
VisFat float64 `json:"visfat"`
|
|
BMI float64 `json:"bmi"`
|
|
Protein float64 `json:"protein"`
|
|
FatFree float64 `json:"fatFreeWeight"`
|
|
Sinew float64 `json:"sinew"`
|
|
BUserID json.Number `json:"bUserId"`
|
|
}
|
|
|
|
// Account config stored in Renpho dossier's Data field
|
|
type renphoConfig struct {
|
|
Accounts []renphoAccount `json:"renpho_accounts"`
|
|
}
|
|
|
|
type renphoAccount struct {
|
|
Email string `json:"email"`
|
|
Password string `json:"password"`
|
|
DossierID string `json:"dossier_id"` // inou dossier to write vitals to
|
|
UserMap map[string]string `json:"user_map"` // renpho userID → inou dossierID (for multi-user scales)
|
|
}
|
|
|
|
// Runtime state for an authenticated session
|
|
type session struct {
|
|
token string
|
|
}
|
|
|
|
func main() {
|
|
setup := flag.Bool("setup", false, "Create Renpho system dossier and configure accounts")
|
|
discover := flag.Bool("discover", false, "Login and show Renpho user IDs for mapping")
|
|
fileImport := flag.String("file", "", "Import from JSON file instead of API (format: measurements array)")
|
|
dossierID := flag.String("dossier", "", "Target dossier ID (required with -file)")
|
|
daemon := flag.Bool("daemon", false, "Run continuously, importing on an interval")
|
|
interval := flag.Int("interval", 3600, "Seconds between imports in daemon mode")
|
|
flag.Parse()
|
|
|
|
if err := lib.Init(); err != nil {
|
|
fatal("lib.Init: %v", err)
|
|
}
|
|
|
|
if *setup {
|
|
runSetup()
|
|
return
|
|
}
|
|
|
|
if *discover {
|
|
runDiscover()
|
|
return
|
|
}
|
|
|
|
if *fileImport != "" {
|
|
if *dossierID == "" {
|
|
fatal("-dossier required with -file")
|
|
}
|
|
runFileImport(*fileImport, *dossierID)
|
|
return
|
|
}
|
|
|
|
if *daemon {
|
|
runDaemon(*interval)
|
|
return
|
|
}
|
|
|
|
runImport()
|
|
}
|
|
|
|
func runDaemon(interval int) {
|
|
sig := make(chan os.Signal, 1)
|
|
signal.Notify(sig, syscall.SIGTERM, syscall.SIGINT)
|
|
|
|
for {
|
|
log.Printf("running import...")
|
|
runImport()
|
|
|
|
select {
|
|
case s := <-sig:
|
|
log.Printf("received %v, exiting", s)
|
|
return
|
|
case <-time.After(time.Duration(interval) * time.Second):
|
|
}
|
|
}
|
|
}
|
|
|
|
func runImport() {
|
|
renphoID, cfg, err := loadConfig()
|
|
if err != nil {
|
|
log.Printf("ERROR load config: %v", err)
|
|
return
|
|
}
|
|
|
|
if len(cfg.Accounts) == 0 {
|
|
log.Printf("no Renpho accounts configured")
|
|
return
|
|
}
|
|
|
|
importID := lib.NextImportID()
|
|
|
|
for _, acct := range cfg.Accounts {
|
|
fmt.Printf("=== %s ===\n", acct.Email)
|
|
if err := syncAccount(renphoID, &acct, importID); err != nil {
|
|
log.Printf(" ERROR syncing %s: %v", acct.Email, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// runSetup creates the Renpho system dossier with config from stdin
|
|
func runSetup() {
|
|
// Check if already exists
|
|
entries, err := lib.EntryRead("", "", &lib.Filter{Category: 0})
|
|
if err != nil {
|
|
fatal("list dossiers: %v", err)
|
|
}
|
|
for _, e := range entries {
|
|
if strings.EqualFold(e.SearchKey, "renpho") {
|
|
fmt.Printf("Renpho dossier already exists: %s\n", e.EntryID)
|
|
fmt.Println("Current config:")
|
|
fmt.Println(e.Data)
|
|
return
|
|
}
|
|
}
|
|
|
|
// Read config JSON from stdin
|
|
fmt.Println("Paste renpho_accounts JSON (or empty for blank), then Ctrl+D:")
|
|
data, _ := io.ReadAll(os.Stdin)
|
|
configJSON := strings.TrimSpace(string(data))
|
|
|
|
// Validate if non-empty
|
|
if configJSON != "" {
|
|
var cfg renphoConfig
|
|
if err := json.Unmarshal([]byte(configJSON), &cfg); err != nil {
|
|
fatal("invalid JSON: %v", err)
|
|
}
|
|
} else {
|
|
configJSON = `{"renpho_accounts":[]}`
|
|
}
|
|
|
|
id := lib.NewID()
|
|
err = lib.EntryWrite("", &lib.Entry{
|
|
EntryID: id,
|
|
DossierID: id,
|
|
Category: 0,
|
|
Type: "dossier",
|
|
Summary: "Renpho",
|
|
SearchKey: "renpho",
|
|
Data: configJSON,
|
|
Timestamp: time.Now().Unix(),
|
|
})
|
|
if err != nil {
|
|
fatal("create dossier: %v", err)
|
|
}
|
|
fmt.Printf("Created Renpho dossier: %s\n", id)
|
|
}
|
|
|
|
// runFileImport imports measurements from a JSON file (offline mode)
|
|
func runFileImport(filePath, dossierID string) {
|
|
data, err := os.ReadFile(filePath)
|
|
if err != nil {
|
|
fatal("read file: %v", err)
|
|
}
|
|
var ms []measurement
|
|
if err := json.Unmarshal(data, &ms); err != nil {
|
|
fatal("parse JSON: %v", err)
|
|
}
|
|
fmt.Printf("Loaded %d measurements from %s\n", len(ms), filePath)
|
|
|
|
importID := lib.NextImportID()
|
|
// Use system accessor (empty string) for file imports
|
|
created, skipped, err := writeMeasurements("", dossierID, ms, importID)
|
|
if err != nil {
|
|
fatal("write: %v", err)
|
|
}
|
|
fmt.Printf("Created %d, skipped %d\n", created, skipped)
|
|
}
|
|
|
|
// runDiscover logs into Renpho and shows user IDs + table mappings
|
|
func runDiscover() {
|
|
if flag.NArg() < 2 {
|
|
fmt.Println("Usage: import-renpho -discover <email> <password>")
|
|
os.Exit(1)
|
|
}
|
|
email, password := flag.Arg(0), flag.Arg(1)
|
|
|
|
s, user, err := login(email, password)
|
|
if err != nil {
|
|
fatal("login: %v", err)
|
|
}
|
|
fmt.Printf("Logged in: %s (user ID: %s)\n", user.Email, user.UserID.String())
|
|
|
|
tables, err := getTableMappings(s)
|
|
if err != nil {
|
|
fatal("table mappings: %v", err)
|
|
}
|
|
fmt.Println("\nUser → Table mappings:")
|
|
for _, t := range tables {
|
|
for _, uid := range t.UserIDs {
|
|
fmt.Printf(" user_id: %s table: %s\n", uid.String(), t.TableName)
|
|
ms, err := fetchMeasurements(s, uid.String(), t.TableName)
|
|
if err == nil && len(ms) > 0 {
|
|
fmt.Printf(" %d measurements, latest weight: %.1f kg\n", len(ms), ms[0].Weight)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// loadConfig reads the Renpho system dossier and its config from Data field
|
|
func loadConfig() (string, *renphoConfig, error) {
|
|
// Find Renpho dossier by searching for email="renpho" in cat=0
|
|
entries, err := lib.EntryRead("", "", &lib.Filter{Category: 0})
|
|
if err != nil {
|
|
return "", nil, fmt.Errorf("list dossiers: %v", err)
|
|
}
|
|
|
|
for _, e := range entries {
|
|
if strings.EqualFold(e.SearchKey, "renpho") {
|
|
var cfg renphoConfig
|
|
if e.Data != "" {
|
|
if err := json.Unmarshal([]byte(e.Data), &cfg); err != nil {
|
|
return "", nil, fmt.Errorf("parse config: %v", err)
|
|
}
|
|
}
|
|
return e.EntryID, &cfg, nil
|
|
}
|
|
}
|
|
|
|
return "", nil, fmt.Errorf("no Renpho system dossier found (create a dossier with SearchKey='renpho')")
|
|
}
|
|
|
|
func syncAccount(renphoID string, acct *renphoAccount, importID int64) error {
|
|
// Login
|
|
s, user, err := login(acct.Email, acct.Password)
|
|
if err != nil {
|
|
return fmt.Errorf("login: %v", err)
|
|
}
|
|
fmt.Printf(" Logged in as %s (user %s)\n", user.Email, user.UserID.String())
|
|
|
|
// Get table mappings
|
|
tables, err := getTableMappings(s)
|
|
if err != nil {
|
|
return fmt.Errorf("table mappings: %v", err)
|
|
}
|
|
|
|
for _, t := range tables {
|
|
for _, uid := range t.UserIDs {
|
|
uidStr := uid.String()
|
|
dossierID := acct.DossierID
|
|
if acct.UserMap != nil {
|
|
if mapped, ok := acct.UserMap[uidStr]; ok {
|
|
dossierID = mapped
|
|
}
|
|
}
|
|
if dossierID == "" {
|
|
fmt.Printf(" Skipping user %s (no dossier mapped)\n", uidStr)
|
|
continue
|
|
}
|
|
|
|
// Ensure Renpho has write access to this dossier
|
|
if !lib.CheckAccess(renphoID, dossierID, "", lib.PermWrite) {
|
|
fmt.Printf(" Granting Renpho access to %s\n", dossierID)
|
|
if err := lib.GrantAccess(dossierID, renphoID, dossierID, lib.PermRead|lib.PermWrite, 0); err != nil {
|
|
return fmt.Errorf("grant access to %s: %v", dossierID, err)
|
|
}
|
|
}
|
|
|
|
measurements, err := fetchMeasurements(s, uidStr, t.TableName)
|
|
if err != nil {
|
|
fmt.Printf(" Table %s user %s: %v\n", t.TableName, uidStr, err)
|
|
continue
|
|
}
|
|
fmt.Printf(" Table %s: %d measurements for dossier %s\n", t.TableName, len(measurements), dossierID)
|
|
|
|
created, skipped, err := writeMeasurements(renphoID, dossierID, measurements, importID)
|
|
if err != nil {
|
|
fmt.Printf(" Write error: %v\n", err)
|
|
continue
|
|
}
|
|
fmt.Printf(" Created %d, skipped %d\n", created, skipped)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func login(email, password string) (*session, *loginUser, error) {
|
|
body := map[string]string{"email": email, "password": password}
|
|
resp, err := apiCall(nil, "RenphoHealth/centerUser/loginUser", body)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
if resp.Code != 101 {
|
|
return nil, nil, fmt.Errorf("code %d: %s", resp.Code, resp.Msg)
|
|
}
|
|
var user loginUser
|
|
if err := json.Unmarshal(resp.Data, &user); err != nil {
|
|
return nil, nil, fmt.Errorf("parse login: %v", err)
|
|
}
|
|
return &session{token: user.Token}, &user, nil
|
|
}
|
|
|
|
func getTableMappings(s *session) ([]tableMapping, error) {
|
|
resp, err := apiCall(s, "RenphoHealth/scale/getUserIdMappingTableNames", map[string]string{})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Code != 101 {
|
|
return nil, fmt.Errorf("code %d: %s", resp.Code, resp.Msg)
|
|
}
|
|
var tables []tableMapping
|
|
if err := json.Unmarshal(resp.Data, &tables); err != nil {
|
|
return nil, err
|
|
}
|
|
return tables, nil
|
|
}
|
|
|
|
func fetchMeasurements(s *session, userID, tableName string) ([]measurement, error) {
|
|
userIDs, _ := json.Marshal([]string{userID})
|
|
body := map[string]any{
|
|
"pageNum": 1,
|
|
"pageSize": "200",
|
|
"userIds": string(userIDs),
|
|
"tableName": tableName,
|
|
}
|
|
resp, err := apiCall(s, "RenphoHealth/scale/queryAllMeasureDataList/", body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.Code != 101 {
|
|
return nil, fmt.Errorf("code %d: %s", resp.Code, resp.Msg)
|
|
}
|
|
var ms []measurement
|
|
if err := json.Unmarshal(resp.Data, &ms); err != nil {
|
|
return nil, err
|
|
}
|
|
return ms, nil
|
|
}
|
|
|
|
// writeMeasurements converts Renpho data to inou vital entries
|
|
func writeMeasurements(renphoID, dossierID string, ms []measurement, importID int64) (int, int, error) {
|
|
created, skipped := 0, 0
|
|
|
|
// Ensure category root
|
|
rootID, err := ensureRoot(renphoID, dossierID)
|
|
if err != nil {
|
|
return 0, 0, err
|
|
}
|
|
|
|
// Metric definitions: field name → (unit, label)
|
|
type metricDef struct {
|
|
unit string
|
|
label string
|
|
}
|
|
metrics := []struct {
|
|
name string
|
|
def metricDef
|
|
get func(m *measurement) float64
|
|
}{
|
|
{"weight", metricDef{"kg", "Weight"}, func(m *measurement) float64 { return m.Weight }},
|
|
{"body_fat", metricDef{"%", "Body Fat"}, func(m *measurement) float64 { return m.BodyFat }},
|
|
{"water", metricDef{"%", "Water"}, func(m *measurement) float64 { return m.Water }},
|
|
{"bmr", metricDef{"kcal", "BMR"}, func(m *measurement) float64 { return m.BMR }},
|
|
{"body_age", metricDef{"years", "Body Age"}, func(m *measurement) float64 { return m.BodyAge }},
|
|
{"muscle", metricDef{"%", "Muscle"}, func(m *measurement) float64 { return m.Muscle }},
|
|
{"bone", metricDef{"kg", "Bone Mass"}, func(m *measurement) float64 { return m.Bone }},
|
|
{"subcutaneous_fat", metricDef{"%", "Subcutaneous Fat"}, func(m *measurement) float64 { return m.SubFat }},
|
|
{"visceral_fat", metricDef{"", "Visceral Fat"}, func(m *measurement) float64 { return m.VisFat }},
|
|
{"bmi", metricDef{"", "BMI"}, func(m *measurement) float64 { return m.BMI }},
|
|
{"protein", metricDef{"%", "Protein"}, func(m *measurement) float64 { return m.Protein }},
|
|
{"fat_free_weight", metricDef{"kg", "Fat Free Weight"}, func(m *measurement) float64 { return m.FatFree }},
|
|
{"skeletal_muscle", metricDef{"%", "Skeletal Muscle"}, func(m *measurement) float64 { return m.Sinew }},
|
|
}
|
|
|
|
groups := map[string]string{} // metric name → groupID
|
|
|
|
for _, m := range ms {
|
|
ts := m.TimeStamp
|
|
if ts == 0 {
|
|
continue
|
|
}
|
|
// Renpho timestamps are in seconds
|
|
if ts > 1e12 {
|
|
ts = ts / 1000 // convert millis to seconds if needed
|
|
}
|
|
|
|
for _, met := range metrics {
|
|
val := met.get(&m)
|
|
if val == 0 {
|
|
continue
|
|
}
|
|
|
|
// Ensure group
|
|
groupID, ok := groups[met.name]
|
|
if !ok {
|
|
groupID, err = ensureGroup(renphoID, dossierID, rootID, met.name, met.def.unit, met.def.label)
|
|
if err != nil {
|
|
return created, skipped, fmt.Errorf("group %s: %v", met.name, err)
|
|
}
|
|
groups[met.name] = groupID
|
|
}
|
|
|
|
// Dedup: check for existing reading at same timestamp in same group
|
|
existing, _ := lib.EntryRead(renphoID, dossierID, &lib.Filter{
|
|
Category: lib.CategoryVital,
|
|
Type: "reading",
|
|
ParentID: groupID,
|
|
FromDate: ts,
|
|
ToDate: ts + 1,
|
|
})
|
|
if len(existing) > 0 {
|
|
skipped++
|
|
continue
|
|
}
|
|
|
|
valueStr := fmt.Sprintf("%g", val)
|
|
summary := valueStr
|
|
if met.def.unit != "" {
|
|
summary += " " + met.def.unit
|
|
}
|
|
data, _ := json.Marshal(map[string]string{"unit": met.def.unit, "source": "renpho"})
|
|
|
|
reading := &lib.Entry{
|
|
DossierID: dossierID,
|
|
ParentID: groupID,
|
|
Category: lib.CategoryVital,
|
|
Type: "reading",
|
|
Value: valueStr,
|
|
Summary: summary,
|
|
Timestamp: ts,
|
|
Data: string(data),
|
|
Import: importID,
|
|
}
|
|
if err := lib.EntryWrite(renphoID, reading); err != nil {
|
|
return created, skipped, fmt.Errorf("write %s@%d: %v", met.name, ts, err)
|
|
}
|
|
created++
|
|
}
|
|
}
|
|
|
|
return created, skipped, nil
|
|
}
|
|
|
|
func ensureRoot(accessorID, dossierID string) (string, error) {
|
|
entries, err := lib.EntryRead(accessorID, dossierID, &lib.Filter{
|
|
Category: lib.CategoryVital,
|
|
Type: "root",
|
|
ParentID: dossierID,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(entries) > 0 {
|
|
return entries[0].EntryID, nil
|
|
}
|
|
id := deterministicID(dossierID, "cat", fmt.Sprintf("%d", lib.CategoryVital))
|
|
err = lib.EntryWrite(accessorID, &lib.Entry{
|
|
EntryID: id,
|
|
DossierID: dossierID,
|
|
ParentID: dossierID,
|
|
Category: lib.CategoryVital,
|
|
Type: "root",
|
|
})
|
|
return id, err
|
|
}
|
|
|
|
func ensureGroup(accessorID, dossierID, rootID, metric, unit, label string) (string, error) {
|
|
entries, err := lib.EntryRead(accessorID, dossierID, &lib.Filter{
|
|
Category: lib.CategoryVital,
|
|
Type: metric,
|
|
ParentID: rootID,
|
|
})
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(entries) > 0 {
|
|
return entries[0].EntryID, nil
|
|
}
|
|
id := deterministicID(dossierID, "group", fmt.Sprintf("%d", lib.CategoryVital), metric)
|
|
data, _ := json.Marshal(map[string]string{"unit": unit, "source": "renpho"})
|
|
err = lib.EntryWrite(accessorID, &lib.Entry{
|
|
EntryID: id,
|
|
DossierID: dossierID,
|
|
ParentID: rootID,
|
|
Category: lib.CategoryVital,
|
|
Type: metric,
|
|
Summary: label,
|
|
Data: string(data),
|
|
})
|
|
return id, err
|
|
}
|
|
|
|
func deterministicID(parts ...string) string {
|
|
h := sha256.Sum256([]byte(strings.Join(parts, ":")))
|
|
return fmt.Sprintf("%016x", h[:8])
|
|
}
|
|
|
|
// ============================================================================
|
|
// Renpho API client
|
|
// ============================================================================
|
|
|
|
func apiCall(s *session, path string, body any) (*apiResponse, error) {
|
|
bodyJSON, err := json.Marshal(body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
encrypted := aesEncrypt(bodyJSON)
|
|
wrapper, _ := json.Marshal(map[string]string{"encryptData": encrypted})
|
|
|
|
req, err := http.NewRequest("POST", baseURL+"/"+path, bytes.NewReader(wrapper))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Accept", "application/json")
|
|
req.Header.Set("timeZone", "+1")
|
|
req.Header.Set("language", "2")
|
|
req.Header.Set("appVersion", "5.18.3")
|
|
req.Header.Set("platform", "2")
|
|
req.Header.Set("systemVersion", "14")
|
|
req.Header.Set("phone", "import-renpho")
|
|
req.Header.Set("languageCode", "en")
|
|
req.Header.Set("area", "0")
|
|
if s != nil {
|
|
req.Header.Set("token", s.token)
|
|
}
|
|
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
data, err := io.ReadAll(resp.Body)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(data))
|
|
}
|
|
|
|
// Outer response is plain JSON with encrypted data field
|
|
var outer struct {
|
|
Code int `json:"code"`
|
|
Msg string `json:"msg"`
|
|
Data json.RawMessage `json:"data"`
|
|
}
|
|
if err := json.Unmarshal(data, &outer); err != nil {
|
|
return nil, fmt.Errorf("parse response: %v (%s)", err, string(data))
|
|
}
|
|
|
|
result := &apiResponse{Code: outer.Code, Msg: outer.Msg}
|
|
|
|
// Data field is a quoted base64 AES-encrypted string
|
|
if len(outer.Data) > 2 && outer.Data[0] == '"' {
|
|
var encStr string
|
|
json.Unmarshal(outer.Data, &encStr)
|
|
if encStr != "" {
|
|
decrypted := aesDecrypt(encStr)
|
|
if decrypted != nil {
|
|
result.Data = decrypted
|
|
}
|
|
}
|
|
} else {
|
|
result.Data = outer.Data
|
|
}
|
|
|
|
return result, nil
|
|
}
|
|
|
|
// ============================================================================
|
|
// AES-128/ECB/PKCS5 — matches Renpho's com.renpho.module.utils.AESUtil
|
|
// ============================================================================
|
|
|
|
func aesEncrypt(plaintext []byte) string {
|
|
block, _ := aes.NewCipher([]byte(aesKey))
|
|
padded := pkcs5Pad(plaintext, aes.BlockSize)
|
|
out := make([]byte, len(padded))
|
|
for i := 0; i < len(padded); i += aes.BlockSize {
|
|
block.Encrypt(out[i:i+aes.BlockSize], padded[i:i+aes.BlockSize])
|
|
}
|
|
return base64.StdEncoding.EncodeToString(out)
|
|
}
|
|
|
|
func aesDecrypt(b64 string) []byte {
|
|
data, err := base64.StdEncoding.DecodeString(b64)
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
block, err := aes.NewCipher([]byte(aesKey))
|
|
if err != nil {
|
|
return nil
|
|
}
|
|
if len(data)%aes.BlockSize != 0 {
|
|
return nil
|
|
}
|
|
out := make([]byte, len(data))
|
|
for i := 0; i < len(data); i += aes.BlockSize {
|
|
block.Decrypt(out[i:i+aes.BlockSize], data[i:i+aes.BlockSize])
|
|
}
|
|
return pkcs5Unpad(out)
|
|
}
|
|
|
|
func pkcs5Pad(data []byte, blockSize int) []byte {
|
|
padding := blockSize - len(data)%blockSize
|
|
pad := bytes.Repeat([]byte{byte(padding)}, padding)
|
|
return append(data, pad...)
|
|
}
|
|
|
|
func pkcs5Unpad(data []byte) []byte {
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
padding := int(data[len(data)-1])
|
|
if padding > len(data) || padding > aes.BlockSize {
|
|
return data
|
|
}
|
|
return data[:len(data)-padding]
|
|
}
|
|
|
|
func fatal(format string, args ...any) {
|
|
fmt.Printf("FATAL: "+format+"\n", args...)
|
|
panic("abort")
|
|
}
|
|
|