chore: sync vless encryption code

This commit is contained in:
wwqgtxx 2025-08-24 09:36:45 +08:00
parent 7f38763e22
commit 1ae050ca3b
12 changed files with 635 additions and 769 deletions

View File

@ -50,23 +50,24 @@ func Main(args []string) {
if len(args) > 1 {
seed = args[1]
}
seedBase64, clientBase64, hash11Base64, err := encryption.GenMLKEM768(seed)
seedBase64, clientBase64, hash32Base64, err := encryption.GenMLKEM768(seed)
if err != nil {
panic(err)
}
fmt.Println("Seed: " + seedBase64)
fmt.Println("Client: " + clientBase64)
fmt.Println("Hash11: " + hash11Base64)
fmt.Println("Hash32: " + hash32Base64)
case "vless-x25519":
var privateKey string
if len(args) > 1 {
privateKey = args[1]
}
privateKeyBase64, passwordBase64, err := encryption.GenX25519(privateKey)
privateKeyBase64, passwordBase64, hash32Base64, err := encryption.GenX25519(privateKey)
if err != nil {
panic(err)
}
fmt.Println("PrivateKey: " + privateKeyBase64)
fmt.Println("Password: " + passwordBase64)
fmt.Println("Hash32: " + hash32Base64)
}
}

View File

@ -640,10 +640,10 @@ proxies: # socks5
network: tcp
# -------------------------
# vless encryption客户端配置
# 只使用 1-RTT 模式 / 复用八分钟后协商新的 baseKey周期需小于服务端的值
# / 是只能选一个,后面是 base64RawURLEncoding使用 mihomo generate vless-x25519 和 mihomo generate vless-mlkem768 生成,替换值时需去掉括号
# native/xorpub 的 XTLS 可以 Splice。只使用 1-RTT 模式 / 若服务端发的 ticket 中秒数不为零则 0-RTT 复用
# / 是只能选一个,后面 base64 至少一个,无限串联,使用 mihomo generate vless-x25519 和 mihomo generate vless-mlkem768 生成,替换值时需去掉括号
# -------------------------
encryption: "1rtt/8min.native/divide/random.mlkem768Client.(X25519 Password).(ML-KEM-768 Client)"
encryption: "mlkem768x25519plus.native/xorpub/random.1rtt/0rtt.(X25519 Password).(ML-KEM-768 Client)..."
tls: false #可以不开启tls
udp: true
@ -1365,10 +1365,10 @@ listeners:
# grpc-service-name: "GunService" # 如果不为空则开启 grpc 传输层
# -------------------------
# vless encryption服务端配置
# 只允许 1-RTT 模式 / 同时允许 1-RTT 模式与十分钟复用的 0-RTT 模式;原生外观 / ECH 式 XOR / 全随机数
# / 是只能选一个,后面是 base64RawURLEncoding,使用 mihomo generate vless-x25519 和 mihomo generate vless-mlkem768 生成,替换值时需去掉括号
# 原生外观 / 只 XOR 公钥 / 全随机数。只允许 1-RTT 模式 / 同时允许 1-RTT 模式与 600 秒复用的 0-RTT 模式
# / 是只能选一个,后面 base64 至少一个,无限串联,使用 mihomo generate vless-x25519 和 mihomo generate vless-mlkem768 生成,替换值时需去掉括号
# -------------------------
# decryption: "1rtt/10min.native/divide/random.mlkem768Seed.(X25519 PrivateKey).(ML-KEM-768 Seed)"
# decryption: "mlkem768x25519plus.native/xorpub/random.1rtt/600s.(X25519 PrivateKey).(ML-KEM-768 Seed)..."
# 下面两项如果填写则开启 tls需要同时填写
# certificate: ./server.crt
# private-key: ./server.key

View File

@ -94,24 +94,24 @@ func TestInboundVless_Encryption(t *testing.T) {
t.Fatal(err)
return
}
privateKeyBase64, passwordBase64, err := encryption.GenX25519("")
privateKeyBase64, passwordBase64, _, err := encryption.GenX25519("")
if err != nil {
t.Fatal(err)
return
}
var modes = []string{
"native",
"divide",
"xorpub",
"random",
}
for i := range modes {
mode := modes[i]
t.Run(mode, func(t *testing.T) {
inboundOptions := inbound.VlessOption{
Decryption: "10min." + mode + ".mlkem768Seed." + privateKeyBase64 + "." + seedBase64,
Decryption: "mlkem768x25519plus." + mode + ".600s." + privateKeyBase64 + "." + seedBase64,
}
outboundOptions := outbound.VlessOption{
Encryption: "8min." + mode + ".mlkem768Client." + passwordBase64 + "." + clientBase64,
Encryption: "mlkem768x25519plus." + mode + ".0rtt." + passwordBase64 + "." + clientBase64,
}
testInboundVless(t, inboundOptions, outboundOptions)
t.Run("xtls-rprx-vision", func(t *testing.T) {

View File

@ -46,15 +46,7 @@ func init() {
})
vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) {
tlsConn, loaded := network.CastReader[*encryption.ClientConn](conn)
if !loaded {
return
}
return true, tlsConn.Conn, reflect.TypeOf(tlsConn).Elem(), unsafe.Pointer(tlsConn)
})
vless.RegisterTLS(func(conn net.Conn) (loaded bool, netConn net.Conn, reflectType reflect.Type, reflectPointer unsafe.Pointer) {
tlsConn, loaded := network.CastReader[*encryption.ServerConn](conn)
tlsConn, loaded := network.CastReader[*encryption.CommonConn](conn)
if !loaded {
return
}

View File

@ -1,275 +1,215 @@
package encryption
import (
"bytes"
"crypto/cipher"
"crypto/ecdh"
"crypto/rand"
"errors"
"fmt"
"io"
"net"
"runtime"
"strings"
"sync"
"time"
"github.com/metacubex/blake3"
"github.com/metacubex/utls/mlkem"
"golang.org/x/crypto/sha3"
"golang.org/x/sys/cpu"
)
var (
// Keep in sync with crypto/tls/cipher_suites.go.
hasGCMAsmAMD64 = cpu.X86.HasAES && cpu.X86.HasPCLMULQDQ && cpu.X86.HasSSE41 && cpu.X86.HasSSSE3
hasGCMAsmARM64 = cpu.ARM64.HasAES && cpu.ARM64.HasPMULL
hasGCMAsmS390X = cpu.S390X.HasAES && cpu.S390X.HasAESCTR && cpu.S390X.HasGHASH
hasGCMAsmPPC64 = runtime.GOARCH == "ppc64" || runtime.GOARCH == "ppc64le"
HasAESGCMHardwareSupport = hasGCMAsmAMD64 || hasGCMAsmARM64 || hasGCMAsmS390X || hasGCMAsmPPC64
)
var ClientCipher byte
func init() {
if HasAESGCMHardwareSupport {
ClientCipher = 1
}
}
type ClientInstance struct {
sync.RWMutex
nfsEKey *mlkem.EncapsulationKey768
hash11 [11]byte // no more capacity
xorMode uint32
xorPKey *ecdh.PublicKey
minutes time.Duration
expire time.Time
baseKey []byte
ticket []byte
NfsPKeys []any
NfsPKeysBytes [][]byte
Hash32s [][32]byte
RelaysLength int
XorMode uint32
Seconds uint32
RWLock sync.RWMutex
Expire time.Time
PfsKey []byte
Ticket []byte
}
type ClientConn struct {
net.Conn
instance *ClientInstance
baseKey []byte
ticket []byte
random []byte
aead cipher.AEAD
nonce []byte
peerAEAD cipher.AEAD
peerNonce []byte
input bytes.Reader // peerCache
}
func (i *ClientInstance) Init(nfsEKeyBytes, xorPKeyBytes []byte, xorMode, minutes uint32) (err error) {
if i.nfsEKey != nil {
func (i *ClientInstance) Init(nfsPKeysBytes [][]byte, xorMode, seconds uint32) (err error) {
if i.NfsPKeys != nil {
err = errors.New("already initialized")
return
}
if i.nfsEKey, err = mlkem.NewEncapsulationKey768(nfsEKeyBytes); err != nil {
l := len(nfsPKeysBytes)
if l == 0 {
err = errors.New("empty nfsPKeysBytes")
return
}
if xorMode > 0 {
i.xorMode = xorMode
if i.xorPKey, err = ecdh.X25519().NewPublicKey(xorPKeyBytes); err != nil {
return
i.NfsPKeys = make([]any, l)
i.NfsPKeysBytes = nfsPKeysBytes
i.Hash32s = make([][32]byte, l)
for j, k := range nfsPKeysBytes {
if len(k) == 32 {
if i.NfsPKeys[j], err = ecdh.X25519().NewPublicKey(k); err != nil {
return
}
i.RelaysLength += 32 + 32
} else {
if i.NfsPKeys[j], err = mlkem.NewEncapsulationKey768(k); err != nil {
return
}
i.RelaysLength += 1088 + 32
}
hash32 := sha3.Sum256(nfsEKeyBytes)
copy(i.hash11[:], hash32[:])
i.Hash32s[j] = blake3.Sum256(k)
}
i.minutes = time.Duration(minutes) * time.Minute
i.RelaysLength -= 32
i.XorMode = xorMode
i.Seconds = seconds
return
}
func (i *ClientInstance) Handshake(conn net.Conn) (*ClientConn, error) {
if i.nfsEKey == nil {
func (i *ClientInstance) Handshake(conn net.Conn) (*CommonConn, error) {
if i.NfsPKeys == nil {
return nil, errors.New("uninitialized")
}
if i.xorMode > 0 {
conn, _ = NewXorConn(conn, i.xorMode, i.xorPKey, nil)
}
c := &ClientConn{Conn: conn}
c := &CommonConn{Conn: conn}
if i.minutes > 0 {
i.RLock()
if time.Now().Before(i.expire) {
c.instance = i
c.baseKey = i.baseKey
c.ticket = i.ticket
i.RUnlock()
ivAndRealysLength := 16 + i.RelaysLength
pfsKeyExchangeLength := 18 + 1184 + 32 + 16
paddingLength := int(randBetween(100, 1000))
clientHello := make([]byte, ivAndRealysLength+pfsKeyExchangeLength+paddingLength)
iv := clientHello[:16]
rand.Read(iv)
relays := clientHello[16:ivAndRealysLength]
var nfsPublicKey, nfsKey []byte
var lastCTR cipher.Stream
for j, k := range i.NfsPKeys {
var index = 32
if k, ok := k.(*ecdh.PublicKey); ok {
privateKey, _ := ecdh.X25519().GenerateKey(rand.Reader)
nfsPublicKey = privateKey.PublicKey().Bytes()
copy(relays, nfsPublicKey)
var err error
nfsKey, err = privateKey.ECDH(k)
if err != nil {
return nil, err
}
}
if k, ok := k.(*mlkem.EncapsulationKey768); ok {
nfsKey, nfsPublicKey = k.Encapsulate()
copy(relays, nfsPublicKey)
index = 1088
}
if i.XorMode > 0 { // this xor can (others can't) be decrypted by client's config, revealing an X25519 public key / ML-KEM-768 ciphertext, but it is not important
NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // make X25519 public key / ML-KEM-768 ciphertext distinguishable from random bytes
}
if lastCTR != nil {
lastCTR.XORKeyStream(relays, relays[:32]) // make this relay irreplaceable
}
if j == len(i.NfsPKeys)-1 {
break
}
lastCTR = NewCTR(nfsKey, iv)
lastCTR.XORKeyStream(relays[index:], i.Hash32s[j+1][:])
relays = relays[index+32:]
}
nfsGCM := NewGCM(nfsPublicKey, nfsKey)
if i.Seconds > 0 {
i.RWLock.RLock()
if time.Now().Before(i.Expire) {
c.Client = i
c.UnitedKey = append(i.PfsKey, nfsKey...)
nfsGCM.Seal(clientHello[:ivAndRealysLength], nil, EncodeLength(32), nil)
nfsGCM.Seal(clientHello[:ivAndRealysLength+18], nil, i.Ticket, nil)
i.RWLock.RUnlock()
c.PreWrite = clientHello[:ivAndRealysLength+18+32]
c.GCM = NewGCM(clientHello[ivAndRealysLength+18:ivAndRealysLength+18+32], c.UnitedKey)
if i.XorMode == 2 {
c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), nil, len(c.PreWrite), 32)
}
return c, nil
}
i.RUnlock()
i.RWLock.RUnlock()
}
pfsDKeySeed := make([]byte, 64)
rand.Read(pfsDKeySeed)
pfsDKey, _ := mlkem.NewDecapsulationKey768(pfsDKeySeed)
pfsEKeyBytes := pfsDKey.EncapsulationKey().Bytes()
nfsKey, encapsulatedNfsKey := i.nfsEKey.Encapsulate()
nfsAEAD := NewAEAD(ClientCipher, nfsKey, pfsEKeyBytes, encapsulatedNfsKey)
pfsKeyExchange := clientHello[ivAndRealysLength : ivAndRealysLength+pfsKeyExchangeLength]
nfsGCM.Seal(pfsKeyExchange[:0], nil, EncodeLength(pfsKeyExchangeLength-18), nil)
mlkem768DKey, _ := mlkem.GenerateKey768()
x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader)
pfsPublicKey := append(mlkem768DKey.EncapsulationKey().Bytes(), x25519SKey.PublicKey().Bytes()...)
nfsGCM.Seal(pfsKeyExchange[:18], nil, pfsPublicKey, nil)
clientHello := make([]byte, 5+11+1+1184+1088+randBetween(100, 1000))
EncodeHeader(clientHello, 1, 11+1+1184+1088)
copy(clientHello[5:], i.hash11[:])
clientHello[5+11] = ClientCipher
copy(clientHello[5+11+1:], pfsEKeyBytes)
copy(clientHello[5+11+1+1184:], encapsulatedNfsKey)
padding := clientHello[5+11+1+1184+1088:]
rand.Read(padding) // important
EncodeHeader(padding, 23, len(padding)-5)
nfsAEAD.Seal(padding[:5], clientHello[5:5+11+1], padding[5:len(padding)-16], padding[:5])
padding := clientHello[ivAndRealysLength+pfsKeyExchangeLength:]
nfsGCM.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil)
nfsGCM.Seal(padding[:18], nil, padding[18:paddingLength-16], nil)
if _, err := c.Conn.Write(clientHello); err != nil {
if _, err := conn.Write(clientHello); err != nil {
return nil, err
}
// client can send more NFS AEAD paddings / messages if needed
// padding can be sent in a fragmented way, to create variable traffic pattern, before VLESS flow takes control
_, t, l, err := ReadAndDiscardPaddings(c.Conn, nil, nil) // allow paddings before server hello
encryptedLength := make([]byte, 18)
if _, err := io.ReadFull(conn, encryptedLength); err != nil {
return nil, err
}
if _, err := nfsGCM.Open(encryptedLength[:0], make([]byte, 12), encryptedLength, nil); err != nil {
return nil, err
}
length := DecodeLength(encryptedLength[:2])
if length < 1088+32+16 { // server may send more public keys
return nil, errors.New("too short length")
}
encryptedPfsPublicKey := make([]byte, length)
if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil {
return nil, err
}
nfsGCM.Open(encryptedPfsPublicKey[:0], MaxNonce, encryptedPfsPublicKey, nil)
mlkem768Key, err := mlkem768DKey.Decapsulate(encryptedPfsPublicKey[:1088])
if err != nil {
return nil, err
}
if t != 1 {
return nil, fmt.Errorf("unexpected type %v, expect server hello", t)
}
peerServerHello := make([]byte, 1088+21)
if l != len(peerServerHello) {
return nil, fmt.Errorf("unexpected length %v for server hello", l)
}
if _, err := io.ReadFull(c.Conn, peerServerHello); err != nil {
return nil, err
}
encapsulatedPfsKey := peerServerHello[:1088]
c.ticket = append(i.hash11[:], peerServerHello[1088:]...)
pfsKey, err := pfsDKey.Decapsulate(encapsulatedPfsKey)
peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1088 : 1088+32])
if err != nil {
return nil, err
}
c.baseKey = append(pfsKey, nfsKey...)
x25519Key, err := x25519SKey.ECDH(peerX25519PKey)
if err != nil {
return nil, err
}
pfsKey := append(mlkem768Key, x25519Key...)
c.UnitedKey = append(pfsKey, nfsKey...)
c.GCM = NewGCM(pfsPublicKey, c.UnitedKey)
c.PeerGCM = NewGCM(encryptedPfsPublicKey[:1088+32], c.UnitedKey)
VLESS, _ := NewAEAD(ClientCipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey).Open(nil, append(i.hash11[:], ClientCipher), c.ticket[11:], pfsEKeyBytes)
if !bytes.Equal(VLESS, []byte("VLESS")) {
return nil, errors.New("invalid server")
encryptedTicket := make([]byte, 32)
if _, err := io.ReadFull(conn, encryptedTicket); err != nil {
return nil, err
}
if _, err := c.PeerGCM.Open(encryptedTicket[:0], nil, encryptedTicket, nil); err != nil {
return nil, err
}
seconds := DecodeLength(encryptedTicket)
if i.Seconds > 0 && seconds > 0 {
i.RWLock.Lock()
i.Expire = time.Now().Add(time.Duration(seconds) * time.Second)
i.PfsKey = pfsKey
i.Ticket = encryptedTicket[:16]
i.RWLock.Unlock()
}
if i.minutes > 0 {
i.Lock()
i.expire = time.Now().Add(i.minutes)
i.baseKey = c.baseKey
i.ticket = c.ticket
i.Unlock()
if _, err := io.ReadFull(conn, encryptedLength); err != nil {
return nil, err
}
if _, err := c.PeerGCM.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil {
return nil, err
}
encryptedPadding := make([]byte, DecodeLength(encryptedLength[:2])) // TODO: move to Read()
if _, err := io.ReadFull(conn, encryptedPadding); err != nil {
return nil, err
}
if _, err := c.PeerGCM.Open(encryptedPadding[:0], nil, encryptedPadding, nil); err != nil {
return nil, err
}
if i.XorMode == 2 {
c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, iv), NewCTR(c.UnitedKey, encryptedTicket[:16]), 0, 0)
}
return c, nil
}
func (c *ClientConn) Write(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
var data []byte
for n := 0; n < len(b); {
b := b[n:]
if len(b) > 8192 {
b = b[:8192] // for avoiding another copy() in server's Read()
}
n += len(b)
if c.aead == nil {
data = make([]byte, 5+32+32+5+len(b)+16)
EncodeHeader(data, 0, 32+32)
copy(data[5:], c.ticket)
c.random = make([]byte, 32)
rand.Read(c.random)
copy(data[5+32:], c.random)
EncodeHeader(data[5+32+32:], 23, len(b)+16)
c.aead = NewAEAD(ClientCipher, c.baseKey, c.random, c.ticket)
c.nonce = make([]byte, 12)
c.aead.Seal(data[:5+32+32+5], c.nonce, b, data[5+32+32:5+32+32+5])
} else {
data = make([]byte, 5+len(b)+16)
EncodeHeader(data, 23, len(b)+16)
c.aead.Seal(data[:5], c.nonce, b, data[:5])
if bytes.Equal(c.nonce, MaxNonce) {
c.aead = NewAEAD(ClientCipher, c.baseKey, data[5:], data[:5])
}
}
IncreaseNonce(c.nonce)
if _, err := c.Conn.Write(data); err != nil {
return 0, err
}
}
return len(b), nil
}
func (c *ClientConn) Read(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
if c.peerAEAD == nil {
_, t, l, err := ReadAndDiscardPaddings(c.Conn, nil, nil) // allow paddings before random hello
if err != nil {
if c.instance != nil && strings.HasPrefix(err.Error(), "invalid header: ") { // 0-RTT
c.instance.Lock()
if bytes.Equal(c.ticket, c.instance.ticket) {
c.instance.expire = time.Now() // expired
}
c.instance.Unlock()
return 0, errors.New("new handshake needed")
}
return 0, err
}
if t != 0 {
return 0, fmt.Errorf("unexpected type %v, expect server random", t)
}
peerRandomHello := make([]byte, 32)
if l != len(peerRandomHello) {
return 0, fmt.Errorf("unexpected length %v for server random", l)
}
if _, err := io.ReadFull(c.Conn, peerRandomHello); err != nil {
return 0, err
}
if c.random == nil {
return 0, errors.New("empty c.random")
}
c.peerAEAD = NewAEAD(ClientCipher, c.baseKey, peerRandomHello, c.random)
c.peerNonce = make([]byte, 12)
}
if c.input.Len() > 0 {
return c.input.Read(b)
}
h, t, l, err := ReadAndDecodeHeader(c.Conn) // l: 17~17000
if err != nil {
return 0, err
}
if t != 23 {
return 0, fmt.Errorf("unexpected type %v, expect encrypted data", t)
}
peerData := make([]byte, l)
if _, err := io.ReadFull(c.Conn, peerData); err != nil {
return 0, err
}
dst := peerData[:l-16]
if len(dst) <= len(b) {
dst = b[:len(dst)] // avoids another copy()
}
var peerAEAD cipher.AEAD
if bytes.Equal(c.peerNonce, MaxNonce) {
peerAEAD = NewAEAD(ClientCipher, c.baseKey, peerData, h)
}
_, err = c.peerAEAD.Open(dst[:0], c.peerNonce, peerData, h)
if peerAEAD != nil {
c.peerAEAD = peerAEAD
}
IncreaseNonce(c.peerNonce)
if err != nil {
return 0, err
}
if len(dst) > len(b) {
c.input.Reset(dst[copy(b, dst):])
dst = b // for len(dst)
}
return len(dst), nil
}

View File

@ -5,46 +5,176 @@ import (
"crypto/aes"
"crypto/cipher"
"crypto/rand"
"errors"
"fmt"
"io"
"math/big"
"net"
"strings"
"time"
"github.com/metacubex/utls/hkdf"
"golang.org/x/crypto/chacha20poly1305"
"golang.org/x/crypto/sha3"
"github.com/metacubex/blake3"
)
type CommonConn struct {
net.Conn
Client *ClientInstance
UnitedKey []byte
PreWrite []byte
GCM *GCM
PeerGCM *GCM
PeerCache []byte
}
func (c *CommonConn) Write(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
var data []byte
for n := 0; n < len(b); {
b := b[n:]
if len(b) > 8192 {
b = b[:8192] // for avoiding another copy() in peer's Read()
}
n += len(b)
data = make([]byte, 5+len(b)+16)
EncodeHeader(data, len(b)+16)
aead := c.GCM
if bytes.Equal(c.GCM.Nonce[:], MaxNonce) {
aead = nil
}
c.GCM.Seal(data[:5], nil, b, data[:5])
if aead == nil {
c.GCM = NewGCM(data[5:], c.UnitedKey)
}
if c.PreWrite != nil {
data = append(c.PreWrite, data...)
c.PreWrite = nil
}
if _, err := c.Conn.Write(data); err != nil {
return 0, err
}
}
return len(b), nil
}
func (c *CommonConn) Read(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
if c.PeerGCM == nil { // client's 0-RTT
serverRandom := make([]byte, 32)
if _, err := io.ReadFull(c.Conn, serverRandom); err != nil {
return 0, err
}
c.PeerGCM = NewGCM(serverRandom, c.UnitedKey)
if xorConn, ok := c.Conn.(*XorConn); ok {
xorConn.PeerCTR = NewCTR(c.UnitedKey, serverRandom[16:])
}
}
if len(c.PeerCache) != 0 {
n := copy(b, c.PeerCache)
c.PeerCache = c.PeerCache[n:]
return n, nil
}
h, l, err := ReadAndDecodeHeader(c.Conn) // l: 17~17000
if err != nil {
if c.Client != nil && strings.HasPrefix(err.Error(), "invalid header: ") { // client's 0-RTT
c.Client.RWLock.Lock()
if bytes.Equal(c.UnitedKey[:32], c.Client.PfsKey) {
c.Client.Expire = time.Now() // expired
}
c.Client.RWLock.Unlock()
return 0, errors.New("new handshake needed")
}
return 0, err
}
c.Client = nil
peerData := make([]byte, l)
if _, err := io.ReadFull(c.Conn, peerData); err != nil {
return 0, err
}
dst := peerData[:l-16]
if len(dst) <= len(b) {
dst = b[:len(dst)] // avoids another copy()
}
var peerAEAD *GCM
if bytes.Equal(c.PeerGCM.Nonce[:], MaxNonce) {
peerAEAD = NewGCM(peerData, c.UnitedKey)
}
_, err = c.PeerGCM.Open(dst[:0], nil, peerData, h)
if peerAEAD != nil {
c.PeerGCM = peerAEAD
}
if err != nil {
return 0, err
}
if len(dst) > len(b) {
c.PeerCache = dst[copy(b, dst):]
dst = b // for len(dst)
}
return len(dst), nil
}
type GCM struct {
cipher.AEAD
Nonce [12]byte
}
func NewGCM(ctx, key []byte) *GCM {
k := make([]byte, 32)
blake3.DeriveKey(k, string(ctx), key)
block, _ := aes.NewCipher(k)
aead, _ := cipher.NewGCM(block)
return &GCM{AEAD: aead}
//chacha20poly1305.New()
}
func (a *GCM) Seal(dst, nonce, plaintext, additionalData []byte) []byte {
if nonce == nil {
nonce = IncreaseNonce(a.Nonce[:])
}
return a.AEAD.Seal(dst, nonce, plaintext, additionalData)
}
func (a *GCM) Open(dst, nonce, ciphertext, additionalData []byte) ([]byte, error) {
if nonce == nil {
nonce = IncreaseNonce(a.Nonce[:])
}
return a.AEAD.Open(dst, nonce, ciphertext, additionalData)
}
func IncreaseNonce(nonce []byte) []byte {
for i := 0; i < 12; i++ {
nonce[11-i]++
if nonce[11-i] != 0 {
break
}
}
return nonce
}
var MaxNonce = bytes.Repeat([]byte{255}, 12)
func EncodeHeader(h []byte, t byte, l int) {
switch t {
case 1:
h[0] = 1
h[1] = 1
h[2] = 1
case 0:
h[0] = 0
h[1] = 0
h[2] = 0
case 23:
h[0] = 23
h[1] = 3
h[2] = 3
}
func EncodeLength(l int) []byte {
return []byte{byte(l >> 8), byte(l)}
}
func DecodeLength(b []byte) int {
return int(b[0])<<8 | int(b[1])
}
func EncodeHeader(h []byte, l int) {
h[0] = 23
h[1] = 3
h[2] = 3
h[3] = byte(l >> 8)
h[4] = byte(l)
}
func DecodeHeader(h []byte) (t byte, l int, err error) {
func DecodeHeader(h []byte) (l int, err error) {
l = int(h[3])<<8 | int(h[4])
if h[0] == 23 && h[1] == 3 && h[2] == 3 {
t = 23
} else if h[0] == 0 && h[1] == 0 && h[2] == 0 {
t = 0
} else if h[0] == 1 && h[1] == 1 && h[2] == 1 {
t = 1
} else {
if h[0] != 23 || h[1] != 3 || h[2] != 3 {
l = 0
}
if l < 17 || l > 17000 { // TODO: TLSv1.3 max length
@ -53,50 +183,23 @@ func DecodeHeader(h []byte) (t byte, l int, err error) {
return
}
func ReadAndDecodeHeader(conn net.Conn) (h []byte, t byte, l int, err error) {
func ReadAndDecodeHeader(conn net.Conn) (h []byte, l int, err error) {
h = make([]byte, 5)
if _, err = io.ReadFull(conn, h); err != nil {
return
}
t, l, err = DecodeHeader(h)
l, err = DecodeHeader(h)
return
}
func ReadAndDiscardPaddings(conn net.Conn, aead cipher.AEAD, nonce []byte) (h []byte, t byte, l int, err error) {
func ReadAndDiscardPaddings(conn net.Conn) (h []byte, l int, err error) {
for {
if h, t, l, err = ReadAndDecodeHeader(conn); err != nil || t != 23 {
if h, l, err = ReadAndDecodeHeader(conn); err != nil {
return
}
padding := make([]byte, l)
if _, err = io.ReadFull(conn, padding); err != nil {
if _, err = io.ReadFull(conn, make([]byte, l)); err != nil {
return
}
if aead != nil {
if _, err := aead.Open(nil, nonce, padding, h); err != nil {
return h, t, l, err
}
IncreaseNonce(nonce)
}
}
}
func NewAEAD(c byte, secret, salt, info []byte) (aead cipher.AEAD) {
key, _ := hkdf.Key(sha3.New256, secret, salt, string(info), 32)
if c&1 == 1 {
block, _ := aes.NewCipher(key)
aead, _ = cipher.NewGCM(block)
} else {
aead, _ = chacha20poly1305.New(key)
}
return
}
func IncreaseNonce(nonce []byte) {
for i := 0; i < 12; i++ {
nonce[11-i]++
if nonce[11-i] != 0 {
break
}
}
}

View File

@ -17,4 +17,5 @@
// https://github.com/XTLS/Xray-core/commit/373558ed7abdbac3de41745cf30ec04c9adde604
// https://github.com/XTLS/Xray-core/commit/38cc306c955c362f044e074049a5e67b6b9fb389
// https://github.com/XTLS/Xray-core/commit/b33555cc0a52d0af3c23d2af8fca42f8a685d9af
// https://github.com/XTLS/Xray-core/commit/ad7140641c44239c9dcdc3d7215ea639b1f0841c
package encryption

View File

@ -14,46 +14,39 @@ func NewClient(encryption string) (*ClientInstance, error) {
case "", "none": // We will not reject empty string like xray-core does, because we need to ensure compatibility
return nil, nil
}
if s := strings.Split(encryption, "."); len(s) == 5 && s[2] == "mlkem768Client" {
var minutes uint32
if s[0] != "1rtt" {
t := strings.TrimSuffix(s[0], "min")
if t == s[0] {
return nil, fmt.Errorf("invaild vless encryption value: %s", encryption)
}
i, err := strconv.Atoi(t)
if err != nil {
return nil, fmt.Errorf("invaild vless encryption value: %s", encryption)
}
minutes = uint32(i)
}
if s := strings.Split(encryption, "."); len(s) >= 4 && s[0] == "mlkem768x25519plus" {
var xorMode uint32
switch s[1] {
case "native":
case "divide":
case "xorpub":
xorMode = 1
case "random":
xorMode = 2
default:
return nil, fmt.Errorf("invaild vless encryption value: %s", encryption)
}
xorPKeyBytes, err := base64.RawURLEncoding.DecodeString(s[3])
if err != nil {
var seconds uint32
switch s[2] {
case "1rtt":
case "0rtt":
seconds = 1
default:
return nil, fmt.Errorf("invaild vless encryption value: %s", encryption)
}
if len(xorPKeyBytes) != X25519PasswordSize {
return nil, fmt.Errorf("invaild vless encryption value: %s", encryption)
}
nfsEKeyBytes, err := base64.RawURLEncoding.DecodeString(s[4])
if err != nil {
return nil, fmt.Errorf("invaild vless encryption value: %s", encryption)
}
if len(nfsEKeyBytes) != MLKEM768ClientLength {
return nil, fmt.Errorf("invaild vless encryption value: %s", encryption)
var nfsPKeysBytes [][]byte
for _, r := range s[3:] {
b, err := base64.RawURLEncoding.DecodeString(r)
if err != nil {
return nil, fmt.Errorf("invaild vless encryption value: %s", encryption)
}
if len(b) != X25519PasswordSize && len(b) != MLKEM768ClientLength {
return nil, fmt.Errorf("invaild vless encryption value: %s", encryption)
}
nfsPKeysBytes = append(nfsPKeysBytes, b)
}
client := &ClientInstance{}
if err = client.Init(nfsEKeyBytes, xorPKeyBytes, xorMode, minutes); err != nil {
return nil, fmt.Errorf("failed to use mlkem768seed: %w", err)
if err := client.Init(nfsPKeysBytes, xorMode, seconds); err != nil {
return nil, fmt.Errorf("failed to use encryption: %w", err)
}
return client, nil
}
@ -67,10 +60,20 @@ func NewServer(decryption string) (*ServerInstance, error) {
case "", "none": // We will not reject empty string like xray-core does, because we need to ensure compatibility
return nil, nil
}
if s := strings.Split(decryption, "."); len(s) == 5 && s[2] == "mlkem768Seed" {
var minutes uint32
if s[0] != "1rtt" {
t := strings.TrimSuffix(s[0], "min")
if s := strings.Split(decryption, "."); len(s) >= 4 && s[0] == "mlkem768x25519plus" {
var xorMode uint32
switch s[1] {
case "native":
case "xorpub":
xorMode = 1
case "random":
xorMode = 2
default:
return nil, fmt.Errorf("invaild vless decryption value: %s", decryption)
}
var seconds uint32
if s[2] != "1rtt" {
t := strings.TrimSuffix(s[2], "s")
if t == s[0] {
return nil, fmt.Errorf("invaild vless decryption value: %s", decryption)
}
@ -78,35 +81,22 @@ func NewServer(decryption string) (*ServerInstance, error) {
if err != nil {
return nil, fmt.Errorf("invaild vless decryption value: %s", decryption)
}
minutes = uint32(i)
seconds = uint32(i)
}
var xorMode uint32
switch s[1] {
case "native":
case "divide":
xorMode = 1
case "random":
xorMode = 2
default:
return nil, fmt.Errorf("invaild vless decryption value: %s", decryption)
}
xorSKeyBytes, err := base64.RawURLEncoding.DecodeString(s[3])
if err != nil {
return nil, fmt.Errorf("invaild vless decryption value: %s", decryption)
}
if len(xorSKeyBytes) != X25519PrivateKeySize {
return nil, fmt.Errorf("invaild vless decryption value: %s", decryption)
}
nfsDKeySeed, err := base64.RawURLEncoding.DecodeString(s[4])
if err != nil {
return nil, fmt.Errorf("invaild vless decryption value: %s", decryption)
}
if len(nfsDKeySeed) != MLKEM768SeedLength {
return nil, fmt.Errorf("invaild vless decryption value: %s", decryption)
var nfsSKeysBytes [][]byte
for _, r := range s[3:] {
b, err := base64.RawURLEncoding.DecodeString(r)
if err != nil {
return nil, fmt.Errorf("invaild vless decryption value: %s", decryption)
}
if len(b) != X25519PrivateKeySize && len(b) != MLKEM768SeedLength {
return nil, fmt.Errorf("invaild vless decryption value: %s", decryption)
}
nfsSKeysBytes = append(nfsSKeysBytes, b)
}
server := &ServerInstance{}
if err = server.Init(nfsDKeySeed, xorSKeyBytes, xorMode, minutes); err != nil {
return nil, fmt.Errorf("failed to use mlkem768seed: %w", err)
if err := server.Init(nfsSKeysBytes, xorMode, seconds); err != nil {
return nil, fmt.Errorf("failed to use decryption: %w", err)
}
return server, nil
}

View File

@ -6,8 +6,8 @@ import (
"encoding/base64"
"fmt"
"github.com/metacubex/blake3"
"github.com/metacubex/utls/mlkem"
"golang.org/x/crypto/sha3"
)
const MLKEM768SeedLength = mlkem.SeedSize
@ -15,7 +15,7 @@ const MLKEM768ClientLength = mlkem.EncapsulationKeySize768
const X25519PasswordSize = 32
const X25519PrivateKeySize = 32
func GenMLKEM768(seedStr string) (seedBase64, clientBase64, hash11Base64 string, err error) {
func GenMLKEM768(seedStr string) (seedBase64, clientBase64, hash32Base64 string, err error) {
var seed [MLKEM768SeedLength]byte
if len(seedStr) > 0 {
s, _ := base64.RawURLEncoding.DecodeString(seedStr)
@ -33,14 +33,14 @@ func GenMLKEM768(seedStr string) (seedBase64, clientBase64, hash11Base64 string,
key, _ := mlkem.NewDecapsulationKey768(seed[:])
client := key.EncapsulationKey().Bytes()
hash32 := sha3.Sum256(client)
hash32 := blake3.Sum256(client)
seedBase64 = base64.RawURLEncoding.EncodeToString(seed[:])
clientBase64 = base64.RawURLEncoding.EncodeToString(client)
hash11Base64 = base64.RawURLEncoding.EncodeToString(hash32[:11])
hash32Base64 = base64.RawURLEncoding.EncodeToString(hash32[:])
return
}
func GenX25519(privateKeyStr string) (privateKeyBase64, passwordBase64 string, err error) {
func GenX25519(privateKeyStr string) (privateKeyBase64, passwordBase64, hash32Base64 string, err error) {
var privateKey [X25519PrivateKeySize]byte
if len(privateKeyStr) > 0 {
s, _ := base64.RawURLEncoding.DecodeString(privateKeyStr)
@ -70,7 +70,10 @@ func GenX25519(privateKeyStr string) (privateKeyBase64, passwordBase64 string, e
fmt.Println(err.Error())
return
}
password := key.PublicKey().Bytes()
hash32 := blake3.Sum256(password)
privateKeyBase64 = base64.RawURLEncoding.EncodeToString(privateKey[:])
passwordBase64 = base64.RawURLEncoding.EncodeToString(key.PublicKey().Bytes())
passwordBase64 = base64.RawURLEncoding.EncodeToString(password)
hash32Base64 = base64.RawURLEncoding.EncodeToString(hash32[:])
return
}

View File

@ -12,75 +12,78 @@ import (
"sync"
"time"
"github.com/metacubex/blake3"
"github.com/metacubex/utls/mlkem"
"golang.org/x/crypto/sha3"
)
type ServerSession struct {
expire time.Time
cipher byte
baseKey []byte
randoms sync.Map
Expire time.Time
PfsKey []byte
Replays sync.Map
}
type ServerInstance struct {
sync.RWMutex
nfsDKey *mlkem.DecapsulationKey768
hash11 [11]byte // no more capacity
xorMode uint32
xorSKey *ecdh.PrivateKey
minutes time.Duration
sessions map[[32]byte]*ServerSession
closed bool
NfsSKeys []any
NfsPKeysBytes [][]byte
Hash32s [][32]byte
RelaysLength int
XorMode uint32
Seconds uint32
RWLock sync.RWMutex
Sessions map[[16]byte]*ServerSession
Closed bool
}
type ServerConn struct {
net.Conn
cipher byte
baseKey []byte
ticket []byte
peerRandom []byte
peerAEAD cipher.AEAD
peerNonce []byte
input bytes.Reader // peerCache
aead cipher.AEAD
nonce []byte
}
func (i *ServerInstance) Init(nfsDKeySeed, xorSKeyBytes []byte, xorMode, minutes uint32) (err error) {
if i.nfsDKey != nil {
func (i *ServerInstance) Init(nfsSKeysBytes [][]byte, xorMode, seconds uint32) (err error) {
if i.NfsSKeys != nil {
err = errors.New("already initialized")
return
}
if i.nfsDKey, err = mlkem.NewDecapsulationKey768(nfsDKeySeed); err != nil {
l := len(nfsSKeysBytes)
if l == 0 {
err = errors.New("empty nfsSKeysBytes")
return
}
if xorMode > 0 {
i.xorMode = xorMode
if i.xorSKey, err = ecdh.X25519().NewPrivateKey(xorSKeyBytes); err != nil {
return
i.NfsSKeys = make([]any, l)
i.NfsPKeysBytes = make([][]byte, l)
i.Hash32s = make([][32]byte, l)
for j, k := range nfsSKeysBytes {
if len(k) == 32 {
if i.NfsSKeys[j], err = ecdh.X25519().NewPrivateKey(k); err != nil {
return
}
i.NfsPKeysBytes[j] = i.NfsSKeys[j].(*ecdh.PrivateKey).PublicKey().Bytes()
i.RelaysLength += 32 + 32
} else {
if i.NfsSKeys[j], err = mlkem.NewDecapsulationKey768(k); err != nil {
return
}
i.NfsPKeysBytes[j] = i.NfsSKeys[j].(*mlkem.DecapsulationKey768).EncapsulationKey().Bytes()
i.RelaysLength += 1088 + 32
}
hash32 := sha3.Sum256(i.nfsDKey.EncapsulationKey().Bytes())
copy(i.hash11[:], hash32[:])
i.Hash32s[j] = blake3.Sum256(i.NfsPKeysBytes[j])
}
if minutes > 0 {
i.minutes = time.Duration(minutes) * time.Minute
i.sessions = make(map[[32]byte]*ServerSession)
i.RelaysLength -= 32
i.XorMode = xorMode
if seconds > 0 {
i.Seconds = seconds
i.Sessions = make(map[[16]byte]*ServerSession)
go func() {
for {
time.Sleep(time.Minute)
i.Lock()
if i.closed {
i.Unlock()
i.RWLock.Lock()
if i.Closed {
i.RWLock.Unlock()
return
}
now := time.Now()
for ticket, session := range i.sessions {
if now.After(session.expire) {
delete(i.sessions, ticket)
for ticket, session := range i.Sessions {
if now.After(session.Expire) {
delete(i.Sessions, ticket)
}
}
i.Unlock()
i.RWLock.Unlock()
}
}()
}
@ -88,221 +91,190 @@ func (i *ServerInstance) Init(nfsDKeySeed, xorSKeyBytes []byte, xorMode, minutes
}
func (i *ServerInstance) Close() (err error) {
i.Lock()
i.closed = true
i.Unlock()
i.RWLock.Lock()
i.Closed = true
i.RWLock.Unlock()
return
}
func (i *ServerInstance) Handshake(conn net.Conn) (*ServerConn, error) {
if i.nfsDKey == nil {
func (i *ServerInstance) Handshake(conn net.Conn) (*CommonConn, error) {
if i.NfsSKeys == nil {
return nil, errors.New("uninitialized")
}
if i.xorMode > 0 {
var err error
if conn, err = NewXorConn(conn, i.xorMode, nil, i.xorSKey); err != nil {
return nil, err
}
}
c := &ServerConn{Conn: conn}
c := &CommonConn{Conn: conn}
_, t, l, err := ReadAndDiscardPaddings(c.Conn, nil, nil) // allow paddings before client/ticket hello
if err != nil {
ivAndRelays := make([]byte, 16+i.RelaysLength)
if _, err := io.ReadFull(conn, ivAndRelays); err != nil {
return nil, err
}
iv := ivAndRelays[:16]
relays := ivAndRelays[16:]
var nfsPublicKey, nfsKey []byte
var lastCTR cipher.Stream
for j, k := range i.NfsSKeys {
if lastCTR != nil {
lastCTR.XORKeyStream(relays, relays[:32]) // recover this relay
}
var index = 32
if _, ok := k.(*mlkem.DecapsulationKey768); ok {
index = 1088
}
if i.XorMode > 0 {
NewCTR(i.NfsPKeysBytes[j], iv).XORKeyStream(relays, relays[:index]) // we don't use buggy elligator, because we have PSK :)
}
nfsPublicKey = relays[:index]
if k, ok := k.(*ecdh.PrivateKey); ok {
publicKey, err := ecdh.X25519().NewPublicKey(nfsPublicKey)
if err != nil {
return nil, err
}
nfsKey, err = k.ECDH(publicKey)
if err != nil {
return nil, err
}
}
if k, ok := k.(*mlkem.DecapsulationKey768); ok {
var err error
nfsKey, err = k.Decapsulate(nfsPublicKey)
if err != nil {
return nil, err
}
}
if j == len(i.NfsSKeys)-1 {
break
}
relays = relays[index:]
lastCTR = NewCTR(nfsKey, iv)
lastCTR.XORKeyStream(relays, relays[:32])
if !bytes.Equal(relays[:32], i.Hash32s[j+1][:]) {
return nil, fmt.Errorf("unexpected hash32: %v", relays[:32])
}
relays = relays[32:]
}
nfsGCM := NewGCM(nfsPublicKey, nfsKey)
if t == 0 {
if i.minutes == 0 {
encryptedLength := make([]byte, 18)
if _, err := io.ReadFull(conn, encryptedLength); err != nil {
return nil, err
}
if _, err := nfsGCM.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil {
return nil, err
}
length := DecodeLength(encryptedLength[:2])
if length == 32 {
if i.Seconds == 0 {
return nil, errors.New("0-RTT is not allowed")
}
peerTicketHello := make([]byte, 32+32)
if l != len(peerTicketHello) {
return nil, fmt.Errorf("unexpected length %v for ticket hello", l)
}
if _, err := io.ReadFull(c.Conn, peerTicketHello); err != nil {
encryptedTicket := make([]byte, 32)
if _, err := io.ReadFull(conn, encryptedTicket); err != nil {
return nil, err
}
if !bytes.Equal(peerTicketHello[:11], i.hash11[:]) {
return nil, fmt.Errorf("unexpected hash11: %v", peerTicketHello[:11])
ticket, err := nfsGCM.Open(nil, nil, encryptedTicket, nil)
if err != nil {
return nil, err
}
i.RLock()
s := i.sessions[[32]byte(peerTicketHello)]
i.RUnlock()
i.RWLock.RLock()
s := i.Sessions[[16]byte(ticket)]
i.RWLock.RUnlock()
if s == nil {
noises := make([]byte, randBetween(100, 1000))
var err error
for err == nil {
rand.Read(noises)
_, _, err = DecodeHeader(noises)
_, err = DecodeHeader(noises)
}
c.Conn.Write(noises) // make client do new handshake
conn.Write(noises) // make client do new handshake
return nil, errors.New("expired ticket")
}
if _, replay := s.randoms.LoadOrStore([32]byte(peerTicketHello[32:]), true); replay {
if _, replay := s.Replays.LoadOrStore([32]byte(encryptedTicket), true); replay {
return nil, errors.New("replay detected")
}
c.cipher = s.cipher
c.baseKey = s.baseKey
c.ticket = peerTicketHello[:32]
c.peerRandom = peerTicketHello[32:]
c.UnitedKey = append(s.PfsKey, nfsKey...) // the same key links the upload & download
c.PreWrite = make([]byte, 32) // always trust yourself, not the client
rand.Read(c.PreWrite)
c.GCM = NewGCM(c.PreWrite, c.UnitedKey)
c.PeerGCM = NewGCM(encryptedTicket, c.UnitedKey)
if i.XorMode == 2 {
c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, c.PreWrite[16:]), NewCTR(c.UnitedKey, iv), 32, 0)
}
return c, nil
}
peerClientHello := make([]byte, 11+1+1184+1088)
if l != len(peerClientHello) {
return nil, fmt.Errorf("unexpected length %v for client hello", l)
if length < 1184+32+16 { // client may send more public keys
return nil, errors.New("too short length")
}
if _, err := io.ReadFull(c.Conn, peerClientHello); err != nil {
encryptedPfsPublicKey := make([]byte, length)
if _, err := io.ReadFull(conn, encryptedPfsPublicKey); err != nil {
return nil, err
}
if !bytes.Equal(peerClientHello[:11], i.hash11[:]) {
return nil, fmt.Errorf("unexpected hash11: %v", peerClientHello[:11])
if _, err := nfsGCM.Open(encryptedPfsPublicKey[:0], nil, encryptedPfsPublicKey, nil); err != nil {
return nil, err
}
c.cipher = peerClientHello[11]
pfsEKeyBytes := peerClientHello[11+1 : 11+1+1184]
encapsulatedNfsKey := peerClientHello[11+1+1184:]
pfsEKey, err := mlkem.NewEncapsulationKey768(pfsEKeyBytes)
mlkem768EKey, err := mlkem.NewEncapsulationKey768(encryptedPfsPublicKey[:1184])
if err != nil {
return nil, err
}
nfsKey, err := i.nfsDKey.Decapsulate(encapsulatedNfsKey)
mlkem768Key, encapsulatedPfsKey := mlkem768EKey.Encapsulate()
peerX25519PKey, err := ecdh.X25519().NewPublicKey(encryptedPfsPublicKey[1184 : 1184+32])
if err != nil {
return nil, err
}
nfsAEAD := NewAEAD(c.cipher, nfsKey, pfsEKeyBytes, encapsulatedNfsKey)
nfsNonce := append([]byte{}, peerClientHello[:11+1]...)
pfsKey, encapsulatedPfsKey := pfsEKey.Encapsulate()
c.baseKey = append(pfsKey, nfsKey...)
pfsAEAD := NewAEAD(c.cipher, c.baseKey, encapsulatedPfsKey, encapsulatedNfsKey)
pfsNonce := append([]byte{}, peerClientHello[:11+1]...)
c.ticket = append(i.hash11[:], pfsAEAD.Seal(nil, pfsNonce, []byte("VLESS"), pfsEKeyBytes)...)
IncreaseNonce(pfsNonce)
serverHello := make([]byte, 5+1088+21+randBetween(100, 1000))
EncodeHeader(serverHello, 1, 1088+21)
copy(serverHello[5:], encapsulatedPfsKey)
copy(serverHello[5+1088:], c.ticket[11:])
padding := serverHello[5+1088+21:]
rand.Read(padding) // important
EncodeHeader(padding, 23, len(padding)-5)
pfsAEAD.Seal(padding[:5], pfsNonce, padding[5:len(padding)-16], padding[:5])
if _, err := c.Conn.Write(serverHello); err != nil {
return nil, err
}
// server can send more PFS AEAD paddings / messages if needed
_, t, l, err = ReadAndDiscardPaddings(c.Conn, nfsAEAD, nfsNonce) // allow paddings before ticket hello
x25519SKey, _ := ecdh.X25519().GenerateKey(rand.Reader)
x25519Key, err := x25519SKey.ECDH(peerX25519PKey)
if err != nil {
return nil, err
}
if t != 0 {
return nil, fmt.Errorf("unexpected type %v, expect ticket hello", t)
}
peerTicketHello := make([]byte, 32+32)
if l != len(peerTicketHello) {
return nil, fmt.Errorf("unexpected length %v for ticket hello", l)
}
if _, err := io.ReadFull(c.Conn, peerTicketHello); err != nil {
pfsKey := append(mlkem768Key, x25519Key...)
pfsPublicKey := append(encapsulatedPfsKey, x25519SKey.PublicKey().Bytes()...)
c.UnitedKey = append(pfsKey, nfsKey...)
c.GCM = NewGCM(pfsPublicKey, c.UnitedKey)
c.PeerGCM = NewGCM(encryptedPfsPublicKey[:1184+32], c.UnitedKey)
ticket := make([]byte, 16)
rand.Read(ticket)
copy(ticket, EncodeLength(int(i.Seconds*4/5)))
pfsKeyExchangeLength := 18 + 1088 + 32 + 16
encryptedTicketLength := 32
paddingLength := int(randBetween(100, 1000))
serverHello := make([]byte, pfsKeyExchangeLength+encryptedTicketLength+paddingLength)
nfsGCM.Seal(serverHello[:0], make([]byte, 12), EncodeLength(pfsKeyExchangeLength-18), nil) // it is safe because our nonce starts from 1
nfsGCM.Seal(serverHello[:18], MaxNonce, pfsPublicKey, nil)
c.GCM.Seal(serverHello[:pfsKeyExchangeLength], nil, ticket, nil)
padding := serverHello[pfsKeyExchangeLength+encryptedTicketLength:]
c.GCM.Seal(padding[:0], nil, EncodeLength(paddingLength-18), nil)
c.GCM.Seal(padding[:18], nil, padding[18:paddingLength-16], nil)
if _, err := conn.Write(serverHello); err != nil {
return nil, err
}
if !bytes.Equal(peerTicketHello[:32], c.ticket) {
return nil, errors.New("naughty boy")
}
c.peerRandom = peerTicketHello[32:]
// padding can be sent in a fragmented way, to create variable traffic pattern, before VLESS flow takes control
if i.minutes > 0 {
i.Lock()
s := &ServerSession{
expire: time.Now().Add(i.minutes),
cipher: c.cipher,
baseKey: c.baseKey,
if i.Seconds > 0 {
i.RWLock.Lock()
i.Sessions[[16]byte(ticket)] = &ServerSession{
Expire: time.Now().Add(time.Duration(i.Seconds) * time.Second),
PfsKey: pfsKey,
}
s.randoms.Store([32]byte(c.peerRandom), true)
i.sessions[[32]byte(c.ticket)] = s
i.Unlock()
i.RWLock.Unlock()
}
if _, err := io.ReadFull(conn, encryptedLength); err != nil {
return nil, err
}
if _, err := nfsGCM.Open(encryptedLength[:0], nil, encryptedLength, nil); err != nil {
return nil, err
}
encryptedPadding := make([]byte, DecodeLength(encryptedLength[:2]))
if _, err := io.ReadFull(conn, encryptedPadding); err != nil {
return nil, err
}
if _, err := nfsGCM.Open(encryptedPadding[:0], nil, encryptedPadding, nil); err != nil {
return nil, err
}
if i.XorMode == 2 {
c.Conn = NewXorConn(conn, NewCTR(c.UnitedKey, ticket), NewCTR(c.UnitedKey, iv), 0, 0)
}
return c, nil
}
func (c *ServerConn) Read(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
if c.peerAEAD == nil {
c.peerAEAD = NewAEAD(c.cipher, c.baseKey, c.peerRandom, c.ticket)
c.peerNonce = make([]byte, 12)
}
if c.input.Len() > 0 {
return c.input.Read(b)
}
h, t, l, err := ReadAndDecodeHeader(c.Conn) // l: 17~17000
if err != nil {
return 0, err
}
if t != 23 {
return 0, fmt.Errorf("unexpected type %v, expect encrypted data", t)
}
peerData := make([]byte, l)
if _, err := io.ReadFull(c.Conn, peerData); err != nil {
return 0, err
}
dst := peerData[:l-16]
if len(dst) <= len(b) {
dst = b[:len(dst)] // avoids another copy()
}
var peerAEAD cipher.AEAD
if bytes.Equal(c.peerNonce, MaxNonce) {
peerAEAD = NewAEAD(c.cipher, c.baseKey, peerData, h)
}
_, err = c.peerAEAD.Open(dst[:0], c.peerNonce, peerData, h)
if peerAEAD != nil {
c.peerAEAD = peerAEAD
}
IncreaseNonce(c.peerNonce)
if err != nil {
return 0, err
}
if len(dst) > len(b) {
c.input.Reset(dst[copy(b, dst):])
dst = b // for len(dst)
}
return len(dst), nil
}
func (c *ServerConn) Write(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
var data []byte
for n := 0; n < len(b); {
b := b[n:]
if len(b) > 8192 {
b = b[:8192] // for avoiding another copy() in client's Read()
}
n += len(b)
if c.aead == nil {
data = make([]byte, 5+32+5+len(b)+16)
EncodeHeader(data, 0, 32)
rand.Read(data[5 : 5+32])
EncodeHeader(data[5+32:], 23, len(b)+16)
c.aead = NewAEAD(c.cipher, c.baseKey, data[5:5+32], c.peerRandom)
c.nonce = make([]byte, 12)
c.aead.Seal(data[:5+32+5], c.nonce, b, data[5+32:5+32+5])
} else {
data = make([]byte, 5+len(b)+16)
EncodeHeader(data, 23, len(b)+16)
c.aead.Seal(data[:5], c.nonce, b, data[:5])
if bytes.Equal(c.nonce, MaxNonce) {
c.aead = NewAEAD(c.cipher, c.baseKey, data[5:], data[:5])
}
}
IncreaseNonce(c.nonce)
if _, err := c.Conn.Write(data); err != nil {
return 0, err
}
}
return len(b), nil
}

View File

@ -3,135 +3,61 @@ package encryption
import (
"crypto/aes"
"crypto/cipher"
"crypto/ecdh"
"crypto/rand"
"errors"
"io"
"net"
"github.com/metacubex/utls/hkdf"
"golang.org/x/crypto/sha3"
"github.com/metacubex/blake3"
)
type XorConn struct {
net.Conn
Divide bool
head []byte
key []byte
ctr cipher.Stream
peerCtr cipher.Stream
isHeader bool
skipNext bool
out_after0 bool
out_header []byte
out_skip int
in_after0 bool
in_header []byte
in_skip int
}
func NewCTR(key, iv []byte, isServer bool) cipher.Stream {
info := "CLIENT"
if isServer {
info = "SERVER" // avoids attackers sending traffic back to the client, though the encryption layer has its own protection
}
key, _ = hkdf.Key(sha3.New256, key, iv, info, 32) // avoids using pKey directly if attackers sent the basepoint, or whaterver they like
block, _ := aes.NewCipher(key)
func NewCTR(key, iv []byte) cipher.Stream {
k := make([]byte, 32)
blake3.DeriveKey(k, "VLESS", key) // avoids using key directly
block, _ := aes.NewCipher(k)
return cipher.NewCTR(block, iv)
}
func NewXorConn(conn net.Conn, mode uint32, pKey *ecdh.PublicKey, sKey *ecdh.PrivateKey) (*XorConn, error) {
if mode == 0 || (pKey == nil && sKey == nil) || (pKey != nil && sKey != nil) {
return nil, errors.New("invalid parameters")
}
c := &XorConn{
Conn: conn,
Divide: mode == 1,
isHeader: true,
out_header: make([]byte, 0, 5), // important
in_header: make([]byte, 0, 5), // important
}
if pKey != nil {
c.head = make([]byte, 16+32)
rand.Read(c.head)
eSKey, _ := ecdh.X25519().NewPrivateKey(c.head[16:])
NewCTR(pKey.Bytes(), c.head[:16], false).XORKeyStream(c.head[16:], eSKey.PublicKey().Bytes()) // make X25519 public key distinguishable from random bytes
c.key, _ = eSKey.ECDH(pKey)
c.ctr = NewCTR(c.key, c.head[:16], false)
}
if sKey != nil {
peerHead := make([]byte, 16+32)
if _, err := io.ReadFull(c.Conn, peerHead); err != nil {
return nil, err
}
NewCTR(sKey.PublicKey().Bytes(), peerHead[:16], false).XORKeyStream(peerHead[16:], peerHead[16:]) // we don't use buggy elligator, because we have PSK :)
ePKey, err := ecdh.X25519().NewPublicKey(peerHead[16:])
if err != nil {
return nil, err
}
key, err := sKey.ECDH(ePKey)
if err != nil {
return nil, err
}
c.peerCtr = NewCTR(key, peerHead[:16], false)
c.head = make([]byte, 16)
rand.Read(c.head) // make sure the server always replies random bytes even when received replays, though it is not important
c.ctr = NewCTR(key, c.head, true) // the same key links the upload & download, though the encryption layer has its own link
}
return c, nil
//chacha20.NewUnauthenticatedCipher()
}
func (c *XorConn) Write(b []byte) (int, error) { // whole one/two records
type XorConn struct {
net.Conn
CTR cipher.Stream
PeerCTR cipher.Stream
OutSkip int
OutHeader []byte
InSkip int
InHeader []byte
}
func NewXorConn(conn net.Conn, ctr, peerCTR cipher.Stream, outSkip, inSkip int) *XorConn {
return &XorConn{
Conn: conn,
CTR: ctr,
PeerCTR: peerCTR,
OutSkip: outSkip,
OutHeader: make([]byte, 0, 5), // important
InSkip: inSkip,
InHeader: make([]byte, 0, 5), // important
}
}
func (c *XorConn) Write(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
if !c.out_after0 {
t, l, _ := DecodeHeader(b)
if t == 23 { // single 23
l = 5
} else { // 1/0 + 23, or noises only
l += 10
if t == 0 {
c.out_after0 = true
if c.Divide {
l -= 5
}
}
}
c.ctr.XORKeyStream(b[:l], b[:l]) // caller MUST discard b
l = len(b)
if c.head != nil {
b = append(c.head, b...)
c.head = nil
}
if _, err := c.Conn.Write(b); err != nil {
return 0, err
}
return l, nil
}
if c.Divide {
return c.Conn.Write(b)
}
for p := b; ; { // for XTLS
if len(p) <= c.out_skip {
c.out_skip -= len(p)
for p := b; ; {
if len(p) <= c.OutSkip {
c.OutSkip -= len(p)
break
}
p = p[c.out_skip:]
c.out_skip = 0
need := 5 - len(c.out_header)
p = p[c.OutSkip:]
c.OutSkip = 0
need := 5 - len(c.OutHeader)
if len(p) < need {
c.out_header = append(c.out_header, p...)
c.ctr.XORKeyStream(p, p)
c.OutHeader = append(c.OutHeader, p...)
c.CTR.XORKeyStream(p, p)
break
}
_, c.out_skip, _ = DecodeHeader(append(c.out_header, p[:need]...))
c.out_header = c.out_header[:0]
c.ctr.XORKeyStream(p[:need], p[:need])
c.OutSkip, _ = DecodeHeader(append(c.OutHeader, p[:need]...))
c.OutHeader = c.OutHeader[:0]
c.CTR.XORKeyStream(p[:need], p[:need])
p = p[need:]
}
if _, err := c.Conn.Write(b); err != nil {
@ -140,85 +66,28 @@ func (c *XorConn) Write(b []byte) (int, error) { // whole one/two records
return len(b), nil
}
func (c *XorConn) Read(b []byte) (int, error) { // 5-bytes, data, 5-bytes...
func (c *XorConn) Read(b []byte) (int, error) {
if len(b) == 0 {
return 0, nil
}
if !c.in_after0 || !c.isHeader {
if c.peerCtr == nil { // for client
peerIv := make([]byte, 16)
if _, err := io.ReadFull(c.Conn, peerIv); err != nil {
return 0, err
}
c.peerCtr = NewCTR(c.key, peerIv, true)
}
if _, err := io.ReadFull(c.Conn, b); err != nil {
return 0, err
}
if c.skipNext {
c.skipNext = false
return len(b), nil
}
c.peerCtr.XORKeyStream(b, b)
if c.isHeader { // always 5-bytes
if t, _, _ := DecodeHeader(b); t == 23 {
c.skipNext = true
} else {
c.isHeader = false
if t == 0 {
c.in_after0 = true
}
}
} else {
c.isHeader = true
}
return len(b), nil
}
if c.Divide {
return c.Conn.Read(b)
}
n, err := c.Conn.Read(b)
for p := b[:n]; ; { // for XTLS
if len(p) <= c.in_skip {
c.in_skip -= len(p)
for p := b[:n]; ; {
if len(p) <= c.InSkip {
c.InSkip -= len(p)
break
}
p = p[c.in_skip:]
c.in_skip = 0
need := 5 - len(c.in_header)
p = p[c.InSkip:]
c.InSkip = 0
need := 5 - len(c.InHeader)
if len(p) < need {
c.peerCtr.XORKeyStream(p, p)
c.in_header = append(c.in_header, p...)
c.PeerCTR.XORKeyStream(p, p)
c.InHeader = append(c.InHeader, p...)
break
}
c.peerCtr.XORKeyStream(p[:need], p[:need])
_, c.in_skip, _ = DecodeHeader(append(c.in_header, p[:need]...))
c.in_header = c.in_header[:0]
c.PeerCTR.XORKeyStream(p[:need], p[:need])
c.InSkip, _ = DecodeHeader(append(c.InHeader, p[:need]...))
c.InHeader = c.InHeader[:0]
p = p[need:]
}
return n, err
}
func (c *XorConn) WriterReplaceable() bool {
if !c.Divide { // never replaceable
return false
}
if !c.out_after0 {
return false
}
return true
}
func (c *XorConn) ReaderReplaceable() bool {
if !c.Divide { // never replaceable
return false
}
if !c.in_after0 || !c.isHeader {
return false
}
return true
}
func (c *XorConn) Upstream() any {
return c.Conn
}

View File

@ -51,13 +51,8 @@ func NewConn(conn net.Conn, tlsConn net.Conn, userUUID *uuid.UUID) (*Conn, error
t = reflect.TypeOf(underlying.Conn).Elem()
//log.Debugln("t:%v", t)
p = unsafe.Pointer(underlying.Conn)
case *encryption.ClientConn:
//log.Debugln("type *encryption.ClientConn")
c.netConn = underlying.Conn
t = reflect.TypeOf(underlying).Elem()
p = unsafe.Pointer(underlying)
case *encryption.ServerConn:
//log.Debugln("type *encryption.ServerConn")
case *encryption.CommonConn:
//log.Debugln("type *encryption.CommonConn")
c.netConn = underlying.Conn
t = reflect.TypeOf(underlying).Elem()
p = unsafe.Pointer(underlying)