package byd // Signing and hashing primitives, ported from pybyd's // _crypto/{aes,hashing,signing}.py. None of this is our design — it exists to // reproduce what the app sends, byte for byte. import ( "bytes" "crypto/aes" "crypto/cipher" "crypto/md5" "crypto/sha1" "encoding/hex" "encoding/json" "fmt" "sort" "strings" "git.tinetunnel.com/thistine/byd-exporter/internal/bangcle" ) // jmap is an insertion-ordered JSON object. Order matters: the outer payload's // checkcode is an MD5 over the serialised object, so re-ordering the keys // changes it. A Go map would sort them and break the hash. type jmap []jfield type jfield struct { Key string Value any } func (m *jmap) set(k string, v any) { for i := range *m { if (*m)[i].Key == k { (*m)[i].Value = v return } } *m = append(*m, jfield{k, v}) } func (m jmap) get(k string) string { for _, f := range m { if f.Key == k { s, _ := f.Value.(string) return s } } return "" } func (m jmap) clone() jmap { return append(jmap{}, m...) } func (m jmap) MarshalJSON() ([]byte, error) { var b bytes.Buffer b.WriteByte('{') for i, f := range m { if i > 0 { b.WriteByte(',') } k, err := marshalCompact(f.Key) if err != nil { return nil, err } v, err := marshalCompact(f.Value) if err != nil { return nil, err } b.Write(k) b.WriteByte(':') b.Write(v) } b.WriteByte('}') return b.Bytes(), nil } // marshalCompact matches Python's json.dumps(separators=(",",":"), // ensure_ascii=False): no HTML escaping of < > &, no trailing newline. func marshalCompact(v any) ([]byte, error) { var b bytes.Buffer enc := json.NewEncoder(&b) enc.SetEscapeHTML(false) if err := enc.Encode(v); err != nil { return nil, err } return bytes.TrimRight(b.Bytes(), "\n"), nil } func md5Hex(s string) string { sum := md5.Sum([]byte(s)) return strings.ToUpper(hex.EncodeToString(sum[:])) } // pwdLoginKey derives the login AES key from the plaintext password. func pwdLoginKey(password string) string { return md5Hex(md5Hex(password)) } // sha1Mixed renders SHA1 with alternating byte case, then drops every '0' at an // even index of the resulting string. func sha1Mixed(s string) string { sum := sha1.Sum([]byte(s)) var mixed strings.Builder for i, b := range sum { h := fmt.Sprintf("%02x", b) if i%2 == 0 { h = strings.ToUpper(h) } mixed.WriteString(h) } var out strings.Builder for i, c := range mixed.String() { if c == '0' && i%2 == 0 { continue } out.WriteRune(c) } return out.String() } // checkcode is the MD5 of the compact JSON with its four 8-char chunks reordered. func checkcode(payload jmap) (string, error) { b, err := marshalCompact(payload) if err != nil { return "", err } sum := md5.Sum(b) h := hex.EncodeToString(sum[:]) return h[24:32] + h[8:16] + h[16:24] + h[0:8], nil } // buildSignString joins the fields sorted by key and appends the secret. func buildSignString(fields jmap, secret string) string { keys := make([]string, 0, len(fields)) for _, f := range fields { keys = append(keys, f.Key) } sort.Strings(keys) parts := make([]string, 0, len(keys)) for _, k := range keys { parts = append(parts, k+"="+fields.get(k)) } return strings.Join(parts, "&") + "&password=" + secret } // aesEncryptHex is the inner payload layer: AES-CBC with a zero IV and a // hex-encoded key, returning uppercase hex. func aesEncryptHex(plaintext, keyHex string) (string, error) { block, err := aesCipher(keyHex) if err != nil { return "", err } data := bangcle.PadPKCS7([]byte(plaintext)) out := make([]byte, len(data)) cipher.NewCBCEncrypter(block, make([]byte, 16)).CryptBlocks(out, data) return strings.ToUpper(hex.EncodeToString(out)), nil } func aesDecryptUTF8(cipherHex, keyHex string) (string, error) { block, err := aesCipher(keyHex) if err != nil { return "", err } ct, err := hex.DecodeString(cipherHex) if err != nil { return "", fmt.Errorf("aes: ciphertext must be hex: %w", err) } if len(ct) == 0 || len(ct)%16 != 0 { return "", fmt.Errorf("aes: ciphertext length %d is not a positive multiple of 16", len(ct)) } out := make([]byte, len(ct)) cipher.NewCBCDecrypter(block, make([]byte, 16)).CryptBlocks(out, ct) return string(bangcle.StripPKCS7(out)), nil } func aesCipher(keyHex string) (cipher.Block, error) { key, err := hex.DecodeString(strings.TrimPrefix(strings.TrimPrefix(keyHex, "0x"), "0X")) if err != nil { return nil, fmt.Errorf("aes: key must be hex: %w", err) } block, err := aes.NewCipher(key) if err != nil { return nil, fmt.Errorf("aes: %w", err) } return block, nil }