package byd // Android device fingerprint sent with every request, ported from the HA // integration's device_fingerprint.py. // // The profile must stay STABLE for an account: BYD treats a new IMEI as a new // device, and a fingerprint that changes on every restart looks like account // sharing. Rather than persisting state, it is derived deterministically from a // seed (the username by default) — same account, same phone, forever. import ( "crypto/md5" _ "embed" "encoding/hex" "encoding/json" "fmt" "hash/fnv" "math/rand" "strings" ) //go:embed device_pool.json var devicePoolJSON []byte // Device is the phone identity presented to the API. type Device struct { OSType string // outer "ostype", always "and" IMEI string MAC string Model string SDK string Mod string IMEIMD5 string MobileBrand string MobileModel string DeviceType string // "0" NetworkType string // "wifi" AndroidVer string // Android release, sent as the inner payload's "osType" OSVersion string // SDK level, sent as the inner payload's "osVersion" } type poolEntry struct { MobileBrand string `json:"mobile_brand"` MobileModel string `json:"mobile_model"` Model string `json:"model"` Mod string `json:"mod"` TACPrefix string `json:"tac_prefix"` SDKOptions []string `json:"sdk_options"` OSOptions []string `json:"os_options"` } // NewDevice derives a stable, realistic device profile from seed. func NewDevice(seed string) (Device, error) { var pool []poolEntry if err := json.Unmarshal(devicePoolJSON, &pool); err != nil { return Device{}, fmt.Errorf("device pool: %w", err) } if len(pool) == 0 { return Device{}, fmt.Errorf("device pool is empty") } h := fnv.New64a() h.Write([]byte(seed)) rng := rand.New(rand.NewSource(int64(h.Sum64()))) e := pool[rng.Intn(len(pool))] i := rng.Intn(len(e.SDKOptions)) imei := generateIMEI(rng, e.TACPrefix) sum := md5.Sum([]byte(imei)) return Device{ OSType: "and", IMEI: imei, MAC: generateMAC(rng), Model: e.Model, SDK: e.SDKOptions[i], Mod: e.Mod, IMEIMD5: hex.EncodeToString(sum[:]), MobileBrand: e.MobileBrand, MobileModel: e.MobileModel, DeviceType: "0", NetworkType: "wifi", AndroidVer: e.OSOptions[i], OSVersion: e.SDKOptions[i], }, nil } // generateIMEI builds a valid 15-digit IMEI: a real 8-digit TAC identifying the // handset model, a 6-digit serial, and a Luhn check digit. func generateIMEI(rng *rand.Rand, tacPrefix string) string { var b strings.Builder b.WriteString(tacPrefix) for n := 0; n < 6; n++ { fmt.Fprint(&b, rng.Intn(10)) } return b.String() + luhn(b.String()) } // luhn returns the check digit for the first 14 IMEI digits. func luhn(partial string) string { total := 0 for i, c := range partial { d := int(c - '0') if i%2 == 1 { if d *= 2; d > 9 { d -= 9 } } total += d } return fmt.Sprint((10 - total%10) % 10) } // generateMAC returns a locally administered unicast address. func generateMAC(rng *rand.Rand) string { octets := make([]string, 6) for n := range octets { b := rng.Intn(256) if n == 0 { b = (b | 0x02) &^ 0x01 } octets[n] = fmt.Sprintf("%02x", b) } return strings.Join(octets, ":") }