112 lines
2.3 KiB
Go
112 lines
2.3 KiB
Go
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
|
|
}
|