init
This commit is contained in:
@@ -0,0 +1,111 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
package secret
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/hex"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParseEE_UserExample(t *testing.T) {
|
||||
s := "ee5ba9cf5cf84698032e5300c864544dd0632e6170692e73746570612e6f6e65"
|
||||
p, err := Parse(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Kind != KindEE {
|
||||
t.Fatalf("kind %v", p.Kind)
|
||||
}
|
||||
raw, err := hex.DecodeString(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if raw[0] != 0xee {
|
||||
t.Fatal("marker")
|
||||
}
|
||||
if !bytes.Equal(p.Key, raw[1:17]) {
|
||||
t.Fatalf("key: got %x want %x", p.Key, raw[1:17])
|
||||
}
|
||||
if !bytes.Equal(p.Domain, raw[17:]) {
|
||||
t.Fatalf("domain: got %x want %x", p.Domain, raw[17:])
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseDD(t *testing.T) {
|
||||
s := "dd00000000000000000000000000000000"
|
||||
p, err := Parse(s)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if p.Kind != KindDD {
|
||||
t.Fatalf("kind %v", p.Kind)
|
||||
}
|
||||
for _, b := range p.Key {
|
||||
if b != 0 {
|
||||
t.Fatal("expected zero key")
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user