Files
sing-box-extended/transport/openvpn/cipher.go

228 lines
6.2 KiB
Go

package openvpn
import (
"crypto/aes"
"crypto/cipher"
"crypto/hmac"
"crypto/rand"
"crypto/sha1"
"crypto/sha256"
"crypto/sha512"
"encoding/binary"
"errors"
"hash"
"golang.org/x/crypto/chacha20poly1305"
)
const (
AESGCMTagSize = 16
AESGCMIVSize = 12
CBCIVSize = aes.BlockSize
)
type DataCipher interface {
Encrypt(header []byte, packetID uint32, payload []byte) ([]byte, error)
Decrypt(packet []byte, headerSize int) ([]byte, error)
}
type AEADDataCipher struct {
send cipher.AEAD
recv cipher.AEAD
sendImplicitIV [AESGCMIVSize]byte
recvImplicitIV [AESGCMIVSize]byte
}
func NewAEADCipher(keys *KeyMaterial, cipherName string) (*AEADDataCipher, error) {
var send, recv cipher.AEAD
var err error
if cipherName == CipherCHACHA20POLY {
send, err = chacha20poly1305.New(keys.SendCipherKey)
if err != nil {
return nil, err
}
recv, err = chacha20poly1305.New(keys.RecvCipherKey)
if err != nil {
return nil, err
}
} else {
sendBlock, err := aes.NewCipher(keys.SendCipherKey)
if err != nil {
return nil, err
}
recvBlock, err := aes.NewCipher(keys.RecvCipherKey)
if err != nil {
return nil, err
}
send, err = cipher.NewGCMWithTagSize(sendBlock, AESGCMTagSize)
if err != nil {
return nil, err
}
recv, err = cipher.NewGCMWithTagSize(recvBlock, AESGCMTagSize)
if err != nil {
return nil, err
}
}
if len(keys.SendHMACKey) < AESGCMIVSize-4 || len(keys.RecvHMACKey) < AESGCMIVSize-4 {
return nil, errors.New("openvpn implicit IV keys are too short")
}
g := &AEADDataCipher{send: send, recv: recv}
copy(g.sendImplicitIV[4:], keys.SendHMACKey[:AESGCMIVSize-4])
copy(g.recvImplicitIV[4:], keys.RecvHMACKey[:AESGCMIVSize-4])
return g, nil
}
func (g *AEADDataCipher) Encrypt(header []byte, packetID uint32, payload []byte) ([]byte, error) {
var pidBytes [4]byte
binary.BigEndian.PutUint32(pidBytes[:], packetID)
nonce := g.nonce(packetID, g.sendImplicitIV)
ad := append(header, pidBytes[:]...)
sealed := g.send.Seal(nil, nonce[:], payload, ad)
out := make([]byte, 0, len(header)+4+len(sealed))
out = append(out, header...)
out = append(out, pidBytes[:]...)
out = append(out, sealed[len(sealed)-AESGCMTagSize:]...)
out = append(out, sealed[:len(sealed)-AESGCMTagSize]...)
return out, nil
}
func (g *AEADDataCipher) Decrypt(packet []byte, headerSize int) ([]byte, error) {
if len(packet) < headerSize+4+AESGCMTagSize+1 {
return nil, errors.New("openvpn gcm data packet too short")
}
header := packet[:headerSize]
pidBytes := packet[headerSize : headerSize+4]
tag := packet[headerSize+4 : headerSize+4+AESGCMTagSize]
ciphertext := packet[headerSize+4+AESGCMTagSize:]
combined := append(ciphertext, tag...)
ad := append(header, pidBytes...)
nonce := g.nonce(binary.BigEndian.Uint32(pidBytes), g.recvImplicitIV)
return g.recv.Open(nil, nonce[:], combined, ad)
}
func (g *AEADDataCipher) nonce(packetID uint32, implicit [AESGCMIVSize]byte) [AESGCMIVSize]byte {
nonce := implicit
binary.BigEndian.PutUint32(nonce[:4], binary.BigEndian.Uint32(nonce[:4])^packetID)
return nonce
}
type CBCDataCipher struct {
sendBlock cipher.Block
recvBlock cipher.Block
sendHMAC []byte
recvHMAC []byte
newHash func() hash.Hash
hmacSize int
}
func NewCBCCipher(keys *KeyMaterial, auth string) (*CBCDataCipher, error) {
sendBlock, err := aes.NewCipher(keys.SendCipherKey)
if err != nil {
return nil, err
}
recvBlock, err := aes.NewCipher(keys.RecvCipherKey)
if err != nil {
return nil, err
}
var newHash func() hash.Hash
var hmacSize int
switch auth {
case AuthSHA256:
newHash = sha256.New
hmacSize = sha256.Size
case AuthSHA384:
newHash = sha512.New384
hmacSize = 48
case AuthSHA512:
newHash = sha512.New
hmacSize = sha512.Size
default:
newHash = sha1.New
hmacSize = sha1.Size
}
return &CBCDataCipher{
sendBlock: sendBlock,
recvBlock: recvBlock,
sendHMAC: cloneBytes(keys.SendHMACKey[:hmacSize]),
recvHMAC: cloneBytes(keys.RecvHMACKey[:hmacSize]),
newHash: newHash,
hmacSize: hmacSize,
}, nil
}
func (c *CBCDataCipher) Encrypt(header []byte, packetID uint32, payload []byte) ([]byte, error) {
var pidBytes [4]byte
binary.BigEndian.PutUint32(pidBytes[:], packetID)
plain := append(pidBytes[:], payload...)
padLen := aes.BlockSize - (len(plain) % aes.BlockSize)
for i := 0; i < padLen; i++ {
plain = append(plain, byte(padLen))
}
iv := make([]byte, CBCIVSize)
if _, err := rand.Read(iv); err != nil {
return nil, err
}
ct := make([]byte, len(plain))
cipher.NewCBCEncrypter(c.sendBlock, iv).CryptBlocks(ct, plain)
mac := hmac.New(c.newHash, c.sendHMAC)
mac.Write(iv)
mac.Write(ct)
tag := mac.Sum(nil)
out := make([]byte, 0, len(header)+c.hmacSize+CBCIVSize+len(ct))
out = append(out, header...)
out = append(out, tag...)
out = append(out, iv...)
out = append(out, ct...)
return out, nil
}
func (c *CBCDataCipher) Decrypt(packet []byte, headerSize int) ([]byte, error) {
minSize := headerSize + c.hmacSize + CBCIVSize + aes.BlockSize
if len(packet) < minSize {
return nil, errors.New("openvpn cbc data packet too short")
}
tag := packet[headerSize : headerSize+c.hmacSize]
iv := packet[headerSize+c.hmacSize : headerSize+c.hmacSize+CBCIVSize]
ct := packet[headerSize+c.hmacSize+CBCIVSize:]
if len(ct)%aes.BlockSize != 0 {
return nil, errors.New("openvpn cbc ciphertext not block-aligned")
}
mac := hmac.New(c.newHash, c.recvHMAC)
mac.Write(iv)
mac.Write(ct)
if !hmac.Equal(tag, mac.Sum(nil)) {
return nil, errors.New("openvpn cbc hmac verification failed")
}
plain := make([]byte, len(ct))
cipher.NewCBCDecrypter(c.recvBlock, iv).CryptBlocks(plain, ct)
padLen := int(plain[len(plain)-1])
if padLen < 1 || padLen > aes.BlockSize {
return nil, errors.New("openvpn cbc invalid padding")
}
plain = plain[:len(plain)-padLen]
if len(plain) < 4 {
return nil, errors.New("openvpn cbc payload too short")
}
return plain[4:], nil
}
func CipherKeyLength(cipher string) int {
switch cipher {
case CipherAES128GCM, CipherAES128CBC:
return 16
case CipherAES192GCM, CipherAES192CBC:
return 24
default:
return 32
}
}
func IsAEAD(cipher string) bool {
switch cipher {
case CipherAES128GCM, CipherAES192GCM, CipherAES256GCM, CipherCHACHA20POLY:
return true
default:
return false
}
}