Files
omv-dijiang/works/patch/protocol/obfhttp/crypto.go.patch
T
2026-04-25 09:04:39 +08:00

117 lines
3.1 KiB
Diff

--- /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]
+}