package secret import ( "encoding/base64" "encoding/hex" "errors" "fmt" "strings" ) type Kind int const ( KindEE Kind = iota KindDD ) // Parsed holds normalized MTProxy secret material for handshake and MTProxyIO. type Parsed struct { Kind Kind // Key is always 16 bytes (MTProxy user secret). Key []byte // Domain is raw bytes after the key in ee-secrets (SNI payload); empty for dd. Domain []byte // RawHex is the original secret string (lowercase hex) for logging. RawHex string } var ( ErrInvalidSecret = errors.New("invalid mtproxy secret") ) // Parse decodes Telegram MTProxy secret from tg:// links (hex or legacy base64). func Parse(secretStr string) (*Parsed, error) { s := strings.TrimSpace(strings.ToLower(secretStr)) if s == "" { return nil, fmt.Errorf("%w: empty", ErrInvalidSecret) } // Strip common tg:// noise s = strings.TrimPrefix(s, "0x") var raw []byte if isHex(s) { b, err := hex.DecodeString(s) if err != nil { return nil, fmt.Errorf("%w: hex: %v", ErrInvalidSecret, err) } raw = b } else { // Telegram sometimes uses base64 secrets pad := strings.Repeat("=", (4-len(s)%4)%4) b, err := base64.StdEncoding.DecodeString(s + pad) if err != nil { b, err = base64.URLEncoding.DecodeString(s + pad) } if err != nil { return nil, fmt.Errorf("%w: not hex or base64: %v", ErrInvalidSecret, err) } raw = b } if len(raw) == 0 { return nil, fmt.Errorf("%w: no payload", ErrInvalidSecret) } switch raw[0] { case 0xdd: if len(raw) != 17 { return nil, fmt.Errorf("%w: dd secret must be 17 bytes, got %d", ErrInvalidSecret, len(raw)) } key := make([]byte, 16) copy(key, raw[1:]) return &Parsed{ Kind: KindDD, Key: key, Domain: nil, RawHex: s, }, nil case 0xee: if len(raw) < 18 { return nil, fmt.Errorf("%w: ee secret too short", ErrInvalidSecret) } key := make([]byte, 16) copy(key, raw[1:17]) domain := make([]byte, len(raw)-17) copy(domain, raw[17:]) return &Parsed{ Kind: KindEE, Key: key, Domain: domain, RawHex: s, }, nil default: return nil, fmt.Errorf("%w: unknown first byte 0x%02x (expected ee/dd)", ErrInvalidSecret, raw[0]) } } func isHex(s string) bool { for _, r := range s { switch { case r >= '0' && r <= '9', r >= 'a' && r <= 'f': default: return false } } return len(s)%2 == 0 && len(s) >= 2 }