Files
sing-box-extended/transport/openvpn/keymethod.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)
}