mirror of
https://github.com/shtorm-7/sing-box-extended.git
synced 2026-06-05 18:57:30 +03:00
251 lines
7.0 KiB
Go
251 lines
7.0 KiB
Go
package openvpn
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"encoding/binary"
|
|
"errors"
|
|
"fmt"
|
|
"hash"
|
|
)
|
|
|
|
const (
|
|
KeyMethod2 = 2
|
|
|
|
keySourcePreMasterSize = 48
|
|
keySourceRandomSize = 32
|
|
|
|
maxCipherKeyLength = 64
|
|
maxHMACKeyLength = 64
|
|
keyBlockSize = 2 * (maxCipherKeyLength + maxHMACKeyLength)
|
|
|
|
keyExpansionID = "OpenVPN"
|
|
)
|
|
|
|
type KeySource struct {
|
|
PreMaster [keySourcePreMasterSize]byte
|
|
Random1 [keySourceRandomSize]byte
|
|
Random2 [keySourceRandomSize]byte
|
|
}
|
|
|
|
type KeySource2 struct {
|
|
Client KeySource
|
|
Server KeySource
|
|
}
|
|
|
|
type KeyMaterial struct {
|
|
SendCipherKey []byte
|
|
SendHMACKey []byte
|
|
RecvCipherKey []byte
|
|
RecvHMACKey []byte
|
|
}
|
|
|
|
type KeyMethod2Record struct {
|
|
Sources KeySource2
|
|
Options string
|
|
Username string
|
|
Password string
|
|
PeerInfo string
|
|
}
|
|
|
|
func NewClientKeyMethod2Record(options, peerInfo, username, password string) (*KeyMethod2Record, error) {
|
|
var record KeyMethod2Record
|
|
if _, err := rand.Read(record.Sources.Client.PreMaster[:]); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := rand.Read(record.Sources.Client.Random1[:]); err != nil {
|
|
return nil, err
|
|
}
|
|
if _, err := rand.Read(record.Sources.Client.Random2[:]); err != nil {
|
|
return nil, err
|
|
}
|
|
record.Options = options
|
|
record.PeerInfo = peerInfo
|
|
record.Username = username
|
|
record.Password = password
|
|
return &record, nil
|
|
}
|
|
|
|
func (r *KeyMethod2Record) MarshalClient() ([]byte, error) {
|
|
if r == nil {
|
|
return nil, errors.New("nil key method 2 record")
|
|
}
|
|
out := make([]byte, 0, 4+1+keySourcePreMasterSize+keySourceRandomSize*2+len(r.Options)+16)
|
|
out = binary.BigEndian.AppendUint32(out, 0)
|
|
out = append(out, KeyMethod2)
|
|
out = append(out, r.Sources.Client.PreMaster[:]...)
|
|
out = append(out, r.Sources.Client.Random1[:]...)
|
|
out = append(out, r.Sources.Client.Random2[:]...)
|
|
out = appendOpenVPNString(out, r.Options)
|
|
out = appendOpenVPNString(out, r.Username)
|
|
out = appendOpenVPNString(out, r.Password)
|
|
out = appendOpenVPNString(out, r.PeerInfo)
|
|
return out, nil
|
|
}
|
|
|
|
func ParseServerKeyMethod2Record(packet []byte) (*KeyMethod2Record, error) {
|
|
if len(packet) < 4+1+keySourceRandomSize*2 {
|
|
return nil, errors.New("key method 2 packet too short")
|
|
}
|
|
if binary.BigEndian.Uint32(packet[:4]) != 0 {
|
|
return nil, errors.New("invalid key method 2 prefix")
|
|
}
|
|
if packet[4]&0x0f != KeyMethod2 {
|
|
return nil, fmt.Errorf("unsupported key method %d", packet[4])
|
|
}
|
|
offset := 5
|
|
record := &KeyMethod2Record{}
|
|
copy(record.Sources.Server.Random1[:], packet[offset:offset+keySourceRandomSize])
|
|
offset += keySourceRandomSize
|
|
copy(record.Sources.Server.Random2[:], packet[offset:offset+keySourceRandomSize])
|
|
offset += keySourceRandomSize
|
|
|
|
var err error
|
|
record.Options, offset, err = readOpenVPNString(packet, offset)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("read options: %w", err)
|
|
}
|
|
record.Username, offset, _ = readOpenVPNString(packet, offset)
|
|
record.Password, offset, _ = readOpenVPNString(packet, offset)
|
|
record.PeerInfo, _, _ = readOpenVPNString(packet, offset)
|
|
return record, nil
|
|
}
|
|
|
|
func DeriveClientKeyMaterial(sources KeySource2, clientSession, serverSession SessionID, cipherKeyLen int) (*KeyMaterial, error) {
|
|
if cipherKeyLen != 16 && cipherKeyLen != 32 {
|
|
return nil, fmt.Errorf("unsupported data cipher key length %d", cipherKeyLen)
|
|
}
|
|
var master [48]byte
|
|
if err := openvpnPRF(
|
|
sources.Client.PreMaster[:],
|
|
keyExpansionID+" master secret",
|
|
sources.Client.Random1[:],
|
|
sources.Server.Random1[:],
|
|
nil,
|
|
nil,
|
|
master[:],
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
keyBlock := make([]byte, keyBlockSize)
|
|
if err := openvpnPRF(
|
|
master[:],
|
|
keyExpansionID+" key expansion",
|
|
sources.Client.Random2[:],
|
|
sources.Server.Random2[:],
|
|
clientSession[:],
|
|
serverSession[:],
|
|
keyBlock,
|
|
); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
clientToServer := keyBlock[:maxCipherKeyLength+maxHMACKeyLength]
|
|
serverToClient := keyBlock[maxCipherKeyLength+maxHMACKeyLength:]
|
|
return &KeyMaterial{
|
|
SendCipherKey: cloneBytes(clientToServer[:cipherKeyLen]),
|
|
SendHMACKey: cloneBytes(clientToServer[maxCipherKeyLength : maxCipherKeyLength+maxHMACKeyLength]),
|
|
RecvCipherKey: cloneBytes(serverToClient[:cipherKeyLen]),
|
|
RecvHMACKey: cloneBytes(serverToClient[maxCipherKeyLength : maxCipherKeyLength+maxHMACKeyLength]),
|
|
}, nil
|
|
}
|
|
|
|
func InstallScriptOptionsString(proto, cipher, auth string) string {
|
|
protoName := "UDPv4"
|
|
if proto == ProtoTCP {
|
|
protoName = "TCPv4_CLIENT"
|
|
}
|
|
keysize := "128"
|
|
switch cipher {
|
|
case CipherAES192GCM, CipherAES192CBC:
|
|
keysize = "192"
|
|
case CipherAES256GCM, CipherAES256CBC, CipherCHACHA20POLY:
|
|
keysize = "256"
|
|
}
|
|
return fmt.Sprintf("V4,dev-type tun,link-mtu 1550,tun-mtu 1500,proto %s,cipher %s,auth %s,keysize %s,key-method 2,tls-client", protoName, cipher, auth, keysize)
|
|
}
|
|
|
|
func InstallScriptPeerInfo(cipher string) string {
|
|
if cipher != "" {
|
|
return "IV_VER=sing-box-openvpn\nIV_PROTO=6\nIV_CIPHERS=" + cipher + "\n"
|
|
}
|
|
return "IV_VER=sing-box-openvpn\nIV_PROTO=6\nIV_CIPHERS=AES-256-GCM:AES-192-GCM:AES-128-GCM:AES-256-CBC:AES-192-CBC:AES-128-CBC:CHACHA20-POLY1305\n"
|
|
}
|
|
|
|
func appendOpenVPNString(out []byte, s string) []byte {
|
|
if s == "" {
|
|
return binary.BigEndian.AppendUint16(out, 0)
|
|
}
|
|
if len(s)+1 > 0xffff {
|
|
s = s[:0xfffe]
|
|
}
|
|
out = binary.BigEndian.AppendUint16(out, uint16(len(s)+1))
|
|
out = append(out, s...)
|
|
out = append(out, 0)
|
|
return out
|
|
}
|
|
|
|
func readOpenVPNString(packet []byte, offset int) (string, int, error) {
|
|
if offset+2 > len(packet) {
|
|
return "", offset, ioStringEOF
|
|
}
|
|
size := int(binary.BigEndian.Uint16(packet[offset : offset+2]))
|
|
offset += 2
|
|
if size == 0 {
|
|
return "", offset, nil
|
|
}
|
|
if offset+size > len(packet) {
|
|
return "", offset, ioStringEOF
|
|
}
|
|
raw := packet[offset : offset+size]
|
|
offset += size
|
|
if raw[len(raw)-1] == 0 {
|
|
raw = raw[:len(raw)-1]
|
|
}
|
|
return string(raw), offset, nil
|
|
}
|
|
|
|
var ioStringEOF = errors.New("openvpn string truncated")
|
|
|
|
func openvpnPRF(secret []byte, label string, clientSeed, serverSeed, clientSession, serverSession []byte, out []byte) error {
|
|
seed := make([]byte, 0, len(label)+len(clientSeed)+len(serverSeed)+len(clientSession)+len(serverSession))
|
|
seed = append(seed, label...)
|
|
seed = append(seed, clientSeed...)
|
|
seed = append(seed, serverSeed...)
|
|
seed = append(seed, clientSession...)
|
|
seed = append(seed, serverSession...)
|
|
|
|
split := (len(secret) + 1) / 2
|
|
s1 := secret[:split]
|
|
s2 := secret[len(secret)-split:]
|
|
|
|
md5Out := pHash(md5.New, s1, seed, len(out))
|
|
sha1Out := pHash(sha1.New, s2, seed, len(out))
|
|
for i := range out {
|
|
out[i] = md5Out[i] ^ sha1Out[i]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func pHash(newHash func() hash.Hash, secret, seed []byte, size int) []byte {
|
|
out := make([]byte, 0, size)
|
|
a := hmacSum(newHash, secret, seed)
|
|
for len(out) < size {
|
|
chunkInput := make([]byte, 0, len(a)+len(seed))
|
|
chunkInput = append(chunkInput, a...)
|
|
chunkInput = append(chunkInput, seed...)
|
|
out = append(out, hmacSum(newHash, secret, chunkInput)...)
|
|
a = hmacSum(newHash, secret, a)
|
|
}
|
|
return out[:size]
|
|
}
|
|
|
|
func hmacSum(newHash func() hash.Hash, key, data []byte) []byte {
|
|
mac := hmac.New(newHash, key)
|
|
_, _ = mac.Write(data)
|
|
return mac.Sum(nil)
|
|
}
|