--- /dev/null +++ b/protocol/obfhttp/crypto.go @@ -0,0 +1,113 @@ +// OMV +package obfhttp + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/md5" + "crypto/rand" + "io" + + E "github.com/sagernet/sing/common/exceptions" + "golang.org/x/crypto/chacha20poly1305" +) + +// Encryptor provides per-chunk AEAD encryption. Each call to Encrypt generates a random nonce. +type Encryptor interface { + Encrypt(plaintext []byte) ([]byte, error) + Decrypt(data []byte) ([]byte, error) +} + +type aeadEncryptor struct { + aead cipher.AEAD +} + +func (e *aeadEncryptor) Encrypt(plaintext []byte) ([]byte, error) { + nonce := make([]byte, e.aead.NonceSize()) + if _, err := io.ReadFull(rand.Reader, nonce); err != nil { + return nil, E.Cause(err, "generate nonce") + } + // nonce || ciphertext || tag + ciphertext := e.aead.Seal(nonce, nonce, plaintext, nil) + return ciphertext, nil +} + +func (e *aeadEncryptor) Decrypt(data []byte) ([]byte, error) { + nonceSize := e.aead.NonceSize() + if len(data) < nonceSize { + return nil, E.New("ciphertext too short") + } + nonce := data[:nonceSize] + ciphertext := data[nonceSize:] + plaintext, err := e.aead.Open(nil, nonce, ciphertext, nil) + if err != nil { + return nil, E.Cause(err, "decrypt") + } + return plaintext, nil +} + +// noneEncryptor passes data through without encryption. +type noneEncryptor struct{} + +func (e *noneEncryptor) Encrypt(plaintext []byte) ([]byte, error) { + return plaintext, nil +} + +func (e *noneEncryptor) Decrypt(data []byte) ([]byte, error) { + return data, nil +} + +// NewEncryptor creates an Encryptor using the given method and password. +// Supported methods: aes-128-gcm, aes-256-gcm, chacha20-ietf-poly1305, none. +func NewEncryptor(method, password string) (Encryptor, error) { + switch method { + case "", "none": + return &noneEncryptor{}, nil + case "aes-128-gcm": + key := evpBytesToKey(password, 16) + block, err := aes.NewCipher(key) + if err != nil { + return nil, E.Cause(err, "create aes cipher") + } + aead, err := cipher.NewGCM(block) + if err != nil { + return nil, E.Cause(err, "create gcm") + } + return &aeadEncryptor{aead: aead}, nil + case "aes-256-gcm": + key := evpBytesToKey(password, 32) + block, err := aes.NewCipher(key) + if err != nil { + return nil, E.Cause(err, "create aes cipher") + } + aead, err := cipher.NewGCM(block) + if err != nil { + return nil, E.Cause(err, "create gcm") + } + return &aeadEncryptor{aead: aead}, nil + case "chacha20-ietf-poly1305": + key := evpBytesToKey(password, 32) + aead, err := chacha20poly1305.New(key) + if err != nil { + return nil, E.Cause(err, "create chacha20-poly1305") + } + return &aeadEncryptor{aead: aead}, nil + default: + return nil, E.New("unsupported encryption method: ", method) + } +} + +// evpBytesToKey derives a key from a password using the OpenSSL EVP_BytesToKey method (MD5-based). +func evpBytesToKey(password string, keyLen int) []byte { + var key []byte + var prev []byte + pass := []byte(password) + for len(key) < keyLen { + h := md5.New() + h.Write(prev) + h.Write(pass) + prev = h.Sum(nil) + key = append(key, prev...) + } + return key[:keyLen] +}