From 5a4b88a5a898ef0e22c0908dba59808992599a06 Mon Sep 17 00:00:00 2001 From: banshan Date: Tue, 31 Dec 2024 13:28:45 +0800 Subject: [PATCH] feat: add crypto --- example/default/config.yaml | 12 ++ example/default/main.go | 2 +- plugin/crypto/api.go | 43 +++++ plugin/crypto/index.go | 44 +++++ plugin/crypto/pkg/getkey_test.go | 31 ++++ plugin/crypto/pkg/method/aes_cbc.go | 99 +++++++++++ plugin/crypto/pkg/method/aes_ctr.go | 61 +++++++ plugin/crypto/pkg/method/aes_ctr.java | 28 +++ plugin/crypto/pkg/method/aes_ctr.js | 12 ++ plugin/crypto/pkg/method/aes_ctr_browser.js | 14 ++ plugin/crypto/pkg/method/aes_ctr_node.js | 13 ++ plugin/crypto/pkg/method/cryptor_test.go | 92 ++++++++++ plugin/crypto/pkg/method/icrypto.go | 51 ++++++ plugin/crypto/pkg/method/package.json | 6 + plugin/crypto/pkg/method/stream.go | 184 ++++++++++++++++++++ plugin/crypto/pkg/method/xor.go | 100 +++++++++++ plugin/crypto/pkg/transform.go | 177 +++++++++++++++++++ 17 files changed, 968 insertions(+), 1 deletion(-) create mode 100644 plugin/crypto/api.go create mode 100644 plugin/crypto/index.go create mode 100755 plugin/crypto/pkg/getkey_test.go create mode 100755 plugin/crypto/pkg/method/aes_cbc.go create mode 100755 plugin/crypto/pkg/method/aes_ctr.go create mode 100755 plugin/crypto/pkg/method/aes_ctr.java create mode 100755 plugin/crypto/pkg/method/aes_ctr.js create mode 100755 plugin/crypto/pkg/method/aes_ctr_browser.js create mode 100755 plugin/crypto/pkg/method/aes_ctr_node.js create mode 100755 plugin/crypto/pkg/method/cryptor_test.go create mode 100755 plugin/crypto/pkg/method/icrypto.go create mode 100755 plugin/crypto/pkg/method/package.json create mode 100755 plugin/crypto/pkg/method/stream.go create mode 100755 plugin/crypto/pkg/method/xor.go create mode 100644 plugin/crypto/pkg/transform.go diff --git a/example/default/config.yaml b/example/default/config.yaml index 3699fee..784ddc1 100644 --- a/example/default/config.yaml +++ b/example/default/config.yaml @@ -62,6 +62,18 @@ snap: snapiframeinterval: 3 snapquerytimedelta: 3 # 查询截图时允许的最大时间差(秒) filter: "^live/.*" + onpub: + transform: + .* : $0 + +crypto: + enable: false + isstatic: false + algo: aes_ctr # 加密算法 支持 aes_ctr xor_c + encryptlen: 1024 + secret: + key: your key + iv: your iv onpub: transform: .* : $0 \ No newline at end of file diff --git a/example/default/main.go b/example/default/main.go index 1cd1a89..25c0924 100644 --- a/example/default/main.go +++ b/example/default/main.go @@ -7,7 +7,7 @@ import ( "m7s.live/v5" _ "m7s.live/v5/plugin/cascade" - // _ "m7s.live/v5/plugin/crypto" + _ "m7s.live/v5/plugin/crypto" _ "m7s.live/v5/plugin/debug" _ "m7s.live/v5/plugin/flv" _ "m7s.live/v5/plugin/gb28181" diff --git a/plugin/crypto/api.go b/plugin/crypto/api.go new file mode 100644 index 0000000..1c4032c --- /dev/null +++ b/plugin/crypto/api.go @@ -0,0 +1,43 @@ +package plugin_crypto + +import ( + "encoding/base64" + "fmt" + "net/http" + + cryptopkg "m7s.live/v5/plugin/crypto/pkg" +) + +func (p *CryptoPlugin) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // 设置 CORS 头 + w.Header().Set("Access-Control-Allow-Origin", "*") + w.Header().Set("Access-Control-Allow-Methods", "GET, POST") + w.Header().Set("Access-Control-Allow-Headers", "Content-Type") + w.Header().Set("Content-Type", "application/json") + + // 获取 stream 参数 + stream := r.URL.Query().Get("stream") + if stream == "" { + http.Error(w, "stream parameter is required", http.StatusBadRequest) + return + } + //判断 stream 是否存在 + if !p.Server.Streams.Has(stream) { + http.Error(w, "stream not found", http.StatusNotFound) + return + } + keyConf, err := cryptopkg.ValidateAndCreateKey(p.IsStatic, p.Algo, p.Secret.Key, p.Secret.Iv, stream) + + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + // cryptor, err := method.GetCryptor(p.Algo, keyConf) + // if err != nil { + // http.Error(w, err.Error(), http.StatusBadRequest) + // return + // } + // w.Write([]byte(cryptor.GetKey())) + + w.Write([]byte(fmt.Sprintf("%s.%s", base64.RawStdEncoding.EncodeToString([]byte(keyConf.Key)), base64.RawStdEncoding.EncodeToString([]byte(keyConf.Iv))))) +} diff --git a/plugin/crypto/index.go b/plugin/crypto/index.go new file mode 100644 index 0000000..2981e7a --- /dev/null +++ b/plugin/crypto/index.go @@ -0,0 +1,44 @@ +package plugin_crypto + +import ( + m7s "m7s.live/v5" + crypto "m7s.live/v5/plugin/crypto/pkg" +) + +var _ = m7s.InstallPlugin[CryptoPlugin](crypto.NewTransform) + +type CryptoPlugin struct { + m7s.Plugin + IsStatic bool `desc:"是否静态密钥" default:"false"` + Algo string `desc:"加密算法" default:"aes_ctr"` //加密算法 + EncryptLen int `desc:"加密字节长度" default:"1024"` //加密字节长度 + Secret struct { + Key string `desc:"加密密钥" default:"your key"` //加密密钥 + Iv string `desc:"加密向量" default:"your iv"` //加密向量 + } `desc:"密钥配置"` +} + +// OnInit 初始化插件时的回调函数 +func (p *CryptoPlugin) OnInit() (err error) { + // 初始化全局配置 + crypto.GlobalConfig = crypto.Config{ + IsStatic: p.IsStatic, + Algo: p.Algo, + EncryptLen: p.EncryptLen, + Secret: struct { + Key string `desc:"加密密钥" default:"your key"` + Iv string `desc:"加密向量" default:"your iv"` + }{ + Key: p.Secret.Key, + Iv: p.Secret.Iv, + }, + } + + p.Info("crypto config initialized", + "algo", p.Algo, + "isStatic", p.IsStatic, + "encryptLen", p.EncryptLen, + ) + + return nil +} diff --git a/plugin/crypto/pkg/getkey_test.go b/plugin/crypto/pkg/getkey_test.go new file mode 100755 index 0000000..114df9f --- /dev/null +++ b/plugin/crypto/pkg/getkey_test.go @@ -0,0 +1,31 @@ +package crypto + +import ( + "encoding/base64" + "io" + "net/http" + "strings" + "testing" +) + +func TestGetKey(t *testing.T) { + stream := "/hdl/live/test0.flv" + host := "http://localhost:8080/crypto/?stream=" + + r, err := http.DefaultClient.Get(host + stream) + if err != nil { + t.Error("get", err) + return + } + b, err := io.ReadAll(r.Body) + if err != nil { + t.Error("read", err) + return + } + b64 := strings.Split(string(b), ".") + + key, err := base64.RawStdEncoding.DecodeString(b64[0]) + t.Log("key", key, err) + iv, err := base64.RawStdEncoding.DecodeString(b64[1]) + t.Log("iv", iv, err) +} diff --git a/plugin/crypto/pkg/method/aes_cbc.go b/plugin/crypto/pkg/method/aes_cbc.go new file mode 100755 index 0000000..500b407 --- /dev/null +++ b/plugin/crypto/pkg/method/aes_cbc.go @@ -0,0 +1,99 @@ +package method + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "errors" +) + +//加密过程: +// 1、处理数据,对数据进行填充,采用PKCS7(当密钥长度不够时,缺几位补几个几)的方式。 +// 2、对数据进行加密,采用AES加密方法中CBC加密模式 +// 3、对得到的加密数据,进行base64加密,得到字符串 +// 解密过程相反 + +// AesEncrypt 加密 cbc 模式 +type AesCryptor struct { + key []byte +} + +func newAesCbc(cfg Key) (ICryptor, error) { + var cryptor *AesCryptor + if cfg.Key == "" { + return nil, errors.New("aes cryptor config no key") + } else { + cryptor = &AesCryptor{key: []byte(cfg.Key)} + } + return cryptor, nil +} + +func init() { + RegisterCryptor("aes_cbc", newAesCbc) +} + +func (c *AesCryptor) Encrypt(origin []byte) ([]byte, error) { + //创建加密实例 + block, err := aes.NewCipher(c.key) + if err != nil { + return nil, err + } + //判断加密快的大小 + blockSize := block.BlockSize() + //填充 + encryptBytes := pkcs7Padding(origin, blockSize) + //初始化加密数据接收切片 + crypted := make([]byte, len(encryptBytes)) + //使用cbc加密模式 + blockMode := cipher.NewCBCEncrypter(block, c.key[:blockSize]) + //执行加密 + blockMode.CryptBlocks(crypted, encryptBytes) + return crypted, nil +} + +func (c *AesCryptor) Decrypt(encrypted []byte) ([]byte, error) { + //创建实例 + block, err := aes.NewCipher(c.key) + if err != nil { + return nil, err + } + //获取块的大小 + blockSize := block.BlockSize() + //使用cbc + blockMode := cipher.NewCBCDecrypter(block, c.key[:blockSize]) + //初始化解密数据接收切片 + crypted := make([]byte, len(encrypted)) + //执行解密 + blockMode.CryptBlocks(crypted, encrypted) + //去除填充 + crypted, err = pkcs7UnPadding(crypted) + if err != nil { + return nil, err + } + return crypted, nil +} + +func (c *AesCryptor) GetKey() string { + return base64.RawStdEncoding.EncodeToString(c.key) +} + +// pkcs7Padding 填充 +func pkcs7Padding(data []byte, blockSize int) []byte { + //判断缺少几位长度。最少1,最多 blockSize + padding := blockSize - len(data)%blockSize + //补足位数。把切片[]byte{byte(padding)}复制padding个 + padText := bytes.Repeat([]byte{byte(padding)}, padding) + return append(data, padText...) +} + +// pkcs7UnPadding 填充的反向操作 +func pkcs7UnPadding(data []byte) ([]byte, error) { + length := len(data) + if length == 0 { + return nil, errors.New("加密字符串错误!") + } + //获取填充的个数 + unPadding := int(data[length-1]) + return data[:(length - unPadding)], nil +} diff --git a/plugin/crypto/pkg/method/aes_ctr.go b/plugin/crypto/pkg/method/aes_ctr.go new file mode 100755 index 0000000..f57da50 --- /dev/null +++ b/plugin/crypto/pkg/method/aes_ctr.go @@ -0,0 +1,61 @@ +package method + +import ( + "crypto/aes" + "crypto/cipher" + "encoding/base64" + "errors" + "fmt" +) + +type AesCtrCryptor struct { + key []byte + iv []byte +} + +func newAesCtr(cfg Key) (ICryptor, error) { + var cryptor *AesCtrCryptor + if cfg.Key == "" || cfg.Iv == "" { + return nil, errors.New("aes ctr cryptor config no key") + } + cryptor = &AesCtrCryptor{key: []byte(cfg.Key), iv: []byte(cfg.Iv)} + + return cryptor, nil +} + +func init() { + RegisterCryptor("aes_ctr", newAesCtr) +} + +func (c *AesCtrCryptor) Encrypt(origin []byte) ([]byte, error) { + + block, err := aes.NewCipher(c.key) + if err != nil { + panic(err) + } + + aesCtr := cipher.NewCTR(block, c.iv) + + // EncryptRaw the plaintext + ciphertext := make([]byte, len(origin)) + aesCtr.XORKeyStream(ciphertext, origin) + return ciphertext, nil +} + +func (c *AesCtrCryptor) Decrypt(encrypted []byte) ([]byte, error) { + block, err := aes.NewCipher(c.key) + if err != nil { + panic(err) + } + + aesCtr := cipher.NewCTR(block, c.iv) + + // Decrypt the ciphertext + plaintext := make([]byte, len(encrypted)) + aesCtr.XORKeyStream(plaintext, encrypted) + return plaintext, nil +} + +func (c *AesCtrCryptor) GetKey() string { + return fmt.Sprintf("%s.%s", base64.RawStdEncoding.EncodeToString(c.key), base64.RawStdEncoding.EncodeToString(c.iv)) +} diff --git a/plugin/crypto/pkg/method/aes_ctr.java b/plugin/crypto/pkg/method/aes_ctr.java new file mode 100755 index 0000000..c342d0a --- /dev/null +++ b/plugin/crypto/pkg/method/aes_ctr.java @@ -0,0 +1,28 @@ +import javax.crypto.Cipher; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public class Aes256Ctr { + + public static byte[] decrypt(byte[] ciphertext, byte[] key, byte[] iv) throws Exception { + Cipher cipher = Cipher.getInstance("AES/CTR/NoPadding"); + SecretKeySpec secretKey = new SecretKeySpec(key, "AES"); + IvParameterSpec ivSpec = new IvParameterSpec(iv); + cipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec); + return cipher.doFinal(ciphertext); + } + + public static void main(String[] args) throws Exception { + + int[] intArray = {253, 72, 209, 96, 36}; + byte[] ciphertext = new byte[intArray.length]; + for (int i = 0; i < intArray.length; i++) { + ciphertext[i] = (byte) intArray[i]; + } +// byte[] ciphertext = //byte[]{253,72,209,96,36]; // ciphertext to be decrypted + byte[] key = "01234567012345670123456701234567".getBytes();// 256-bit key + byte[] iv = "0123456701234567".getBytes();// initialization vector + byte[] plaintext = decrypt(ciphertext, key, iv); + System.out.println(new String(plaintext, "UTF-8")); + } +} \ No newline at end of file diff --git a/plugin/crypto/pkg/method/aes_ctr.js b/plugin/crypto/pkg/method/aes_ctr.js new file mode 100755 index 0000000..c6f33ac --- /dev/null +++ b/plugin/crypto/pkg/method/aes_ctr.js @@ -0,0 +1,12 @@ +const crypto = require('crypto'); + +const key = Buffer.from('01234567012345670123456701234567', 'utf8'); +console.log(key) +const nonce = Buffer.from('0123456701234567', 'utf8'); +console.log(nonce) +const ciphertext = Buffer.from([253,72,209,96,36]); + +const decipher = crypto.createDecipheriv('aes-256-ctr', key, nonce); +const plaintext = decipher.update(ciphertext); +const finalPlaintext = decipher.final(); +console.log(Buffer.concat([plaintext, finalPlaintext]).toString()); diff --git a/plugin/crypto/pkg/method/aes_ctr_browser.js b/plugin/crypto/pkg/method/aes_ctr_browser.js new file mode 100755 index 0000000..b35c48c --- /dev/null +++ b/plugin/crypto/pkg/method/aes_ctr_browser.js @@ -0,0 +1,14 @@ + +var aesjs = require('aes-js'); +let ciphertext = Uint8Array.from([253, 72, 209, 96, 36]); + +let key = aesjs.utils.utf8.toBytes('01234567012345670123456701234567'); +console.log(key) + +let iv = aesjs.utils.utf8.toBytes('0123456701234567'); +console.log(iv) + +var aesCtr = new aesjs.ModeOfOperation.ctr(key, new aesjs.Counter(iv)); +var decryptedBytes = aesCtr.decrypt(ciphertext); +console.log(decryptedBytes) +console.log(aesjs.utils.utf8.fromBytes(decryptedBytes)) diff --git a/plugin/crypto/pkg/method/aes_ctr_node.js b/plugin/crypto/pkg/method/aes_ctr_node.js new file mode 100755 index 0000000..79f6888 --- /dev/null +++ b/plugin/crypto/pkg/method/aes_ctr_node.js @@ -0,0 +1,13 @@ +const crypto = require('crypto'); + +let key = Buffer.from('01234567012345670123456701234567', 'utf8'); +console.log(key) +let iv = Buffer.from('0123456701234567', 'utf8'); +console.log(iv) +let ciphertext = Buffer.from([253,72,209,96,36]); + +const decipher = crypto.createDecipheriv('aes-256-ctr', key, iv); +const plaintext = decipher.update(ciphertext); +const finalPlaintext = decipher.final(); +console.log(Buffer.concat([plaintext, finalPlaintext]).toString()); + diff --git a/plugin/crypto/pkg/method/cryptor_test.go b/plugin/crypto/pkg/method/cryptor_test.go new file mode 100755 index 0000000..23513db --- /dev/null +++ b/plugin/crypto/pkg/method/cryptor_test.go @@ -0,0 +1,92 @@ +package method + +import ( + "encoding/base64" + "testing" +) + +func TestStream(t *testing.T) { + encKey, _ := CreateKey(32) + macKey, _ := CreateKey(32) + + plaintext := "0123456789012345" + pt := []byte(plaintext) + var cfg Key + cfg.EncKey = string(encKey) + cfg.MacKey = string(macKey) + c, _ := GetCryptor("stream", cfg) + t.Log("key", c.GetKey()) + encryptData, err := c.Encrypt(pt) + t.Log("stream encrypt base64", base64.RawStdEncoding.EncodeToString(encryptData), err) + decryptData, err := c.Decrypt(encryptData) + t.Log("stream decrypt", string(decryptData), err) + if string(decryptData) != plaintext { + t.Error("decrypt error") + } + +} + +func TestAesCbc(t *testing.T) { + + encKey, _ := CreateKey(16) + + plaintext := "0123456789012345" + pt := []byte(plaintext) + + var cfg Key + cfg.Key = string(encKey) + c, _ := GetCryptor("aes_cbc", cfg) + t.Log(c.GetKey()) + encryptData, err := c.Encrypt(pt) + t.Log("aes_cbc encrypt base64", base64.RawStdEncoding.EncodeToString(encryptData), err) + decryptData, err := c.Decrypt(encryptData) + t.Log("aes_cbc decrypt", string(decryptData), err) + + if string(decryptData) != plaintext { + t.Error("decrypt error") + } +} + +func TestAesCtr(t *testing.T) { + + encKey, _ := CreateKey(32) + iv, _ := CreateKey(16) + plaintext := "0123456789012345" + pt := []byte(plaintext) + var cfg Key + cfg.Key = string(encKey) + cfg.Iv = string(iv) + + c, _ := GetCryptor("aes_ctr", cfg) + t.Log(c.GetKey()) + encryptData, err := c.Encrypt(pt) + t.Log("aes_ctr encrypt ", string(encryptData), err) + decryptData, err := c.Decrypt(encryptData) + t.Log("aes_ctr decrypt", string(decryptData), err) + + if string(decryptData) != plaintext { + t.Error("decrypt error") + } +} + +func TestXor(t *testing.T) { + + encKey, _ := CreateKey(32) + iv, _ := CreateKey(16) + plaintext := "0123456789012345" + pt := []byte(plaintext) + var cfg Key + cfg.Key = string(encKey) + cfg.Iv = string(iv) + + c, _ := GetCryptor("xor", cfg) + t.Log(c.GetKey()) + encryptData, err := c.Encrypt(pt) + t.Log("xor encrypt ", string(encryptData), "len", len(string(encryptData)), err) + decryptData, err := c.Decrypt(encryptData) + t.Log("xor decrypt", string(decryptData), err) + + if string(decryptData) != plaintext { + t.Error("decrypt error") + } +} diff --git a/plugin/crypto/pkg/method/icrypto.go b/plugin/crypto/pkg/method/icrypto.go new file mode 100755 index 0000000..6d69d18 --- /dev/null +++ b/plugin/crypto/pkg/method/icrypto.go @@ -0,0 +1,51 @@ +package method + +import ( + "crypto/md5" + "crypto/rand" + "encoding/hex" + "fmt" +) + +type ICryptor interface { + Encrypt(origin []byte) ([]byte, error) + Decrypt(encrypted []byte) ([]byte, error) + GetKey() string // 获取密钥 格式:base64(key).base64(iv) +} + +const ( + CryptoEncrypt = iota + 1 + CryptoDecrypt +) + +type CryptoBuilder func(cfg Key) (ICryptor, error) + +var ( + builders = make(map[string]CryptoBuilder) +) + +func RegisterCryptor(name string, builder CryptoBuilder) { + builders[name] = builder +} + +func GetCryptor(cryptor string, cfg Key) (ICryptor, error) { + builder, exists := builders[cryptor] + if !exists { + return nil, fmt.Errorf("Unknown ICryptor %q", cryptor) + } + return builder(cfg) +} + +func CreateKey(keySize int) ([]byte, error) { + key := make([]byte, keySize) + _, err := rand.Read(key) + if err != nil { + return nil, err + } + return key, nil +} + +func Md5Sum(s string) string { + ret := md5.Sum([]byte(s)) + return hex.EncodeToString(ret[:]) +} diff --git a/plugin/crypto/pkg/method/package.json b/plugin/crypto/pkg/method/package.json new file mode 100755 index 0000000..a4944e9 --- /dev/null +++ b/plugin/crypto/pkg/method/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "aes-js": "^3.1.2", + "crypto-js": "^4.1.1" + } +} diff --git a/plugin/crypto/pkg/method/stream.go b/plugin/crypto/pkg/method/stream.go new file mode 100755 index 0000000..09c2c9b --- /dev/null +++ b/plugin/crypto/pkg/method/stream.go @@ -0,0 +1,184 @@ +package method + +import ( + "bytes" + "crypto/aes" + "crypto/cipher" + "crypto/hmac" + "crypto/rand" + "crypto/sha256" + "encoding/base64" + "errors" + "fmt" + "hash" + "io" +) + +type Key struct { + Key string + Iv string + EncKey string + MacKey string +} + +func init() { + RegisterCryptor("stream", newStream) +} + +type StreamCryptor struct { + enckey []byte + macKey []byte + encrypter *StreamEncrypter `yaml:"-"` + decrypter *StreamDecrypter `json:"-"` +} + +func NewStreamEncrypter(encKey, macKey []byte) (*StreamEncrypter, error) { + block, err := aes.NewCipher(encKey) + if err != nil { + return nil, err + } + iv := make([]byte, block.BlockSize()) + _, err = rand.Read(iv) + if err != nil { + return nil, err + } + stream := cipher.NewCTR(block, iv) + mac := hmac.New(sha256.New, macKey) + + return &StreamEncrypter{ + Block: block, + Stream: stream, + Mac: mac, + IV: iv, + }, nil +} +func NewStreamDecrypter(encKey, macKey []byte, meta StreamMeta) (*StreamDecrypter, error) { + block, err := aes.NewCipher(encKey) + if err != nil { + return nil, err + } + stream := cipher.NewCTR(block, meta.IV) + mac := hmac.New(sha256.New, macKey) + + return &StreamDecrypter{ + Block: block, + Stream: stream, + Mac: mac, + Meta: meta, + }, nil +} + +type StreamMeta struct { + // IV is the initial value for the crypto function + IV []byte + // Hash is the sha256 hmac of the stream + Hash []byte +} + +type StreamEncrypter struct { + Source io.Reader + Block cipher.Block + Stream cipher.Stream + Mac hash.Hash + IV []byte +} + +// StreamDecrypter is a decrypter for a stream of data with authentication +type StreamDecrypter struct { + Source io.Reader + Block cipher.Block + Stream cipher.Stream + Mac hash.Hash + Meta StreamMeta +} + +// Read encrypts the bytes of the inner reader and places them into p +func (s *StreamEncrypter) Read(p []byte) (int, error) { + n, readErr := s.Source.Read(p) + if n > 0 { + s.Stream.XORKeyStream(p[:n], p[:n]) + err := writeHash(s.Mac, p[:n]) + if err != nil { + return n, err + } + return n, readErr + } + return 0, io.EOF +} + +// Meta returns the encrypted stream metadata for use in decrypting. This should only be called after the stream is finished +func (s *StreamEncrypter) Meta() StreamMeta { + return StreamMeta{IV: s.IV, Hash: s.Mac.Sum(nil)} +} + +// Read reads bytes from the underlying reader and then decrypts them +func (s *StreamDecrypter) Read(p []byte) (int, error) { + n, readErr := s.Source.Read(p) + if n > 0 { + err := writeHash(s.Mac, p[:n]) + if err != nil { + return n, err + } + s.Stream.XORKeyStream(p[:n], p[:n]) + return n, readErr + } + return 0, io.EOF +} + +func newStream(cfg Key) (ICryptor, error) { + var cryptor *StreamCryptor + if (cfg.EncKey == "") || (cfg.MacKey == "") { + return nil, errors.New("stream cryptor config not enckey or mackey") + } else { + encKey := []byte(cfg.EncKey) + macKey := []byte(cfg.MacKey) + + encrypter, err := NewStreamEncrypter(encKey, macKey) + if err != nil { + return nil, err + } + decrypter, err := NewStreamDecrypter(encKey, macKey, encrypter.Meta()) + if err != nil { + return nil, err + } + cryptor = &StreamCryptor{ + enckey: encKey, + macKey: macKey, + encrypter: encrypter, + decrypter: decrypter, + } + + } + return cryptor, nil +} + +func (c *StreamCryptor) Encrypt(origin []byte) ([]byte, error) { + c.encrypter.Source = bytes.NewReader(origin) + return io.ReadAll(c.encrypter) +} + +func (c *StreamCryptor) Decrypt(encrypted []byte) ([]byte, error) { + c.decrypter.Source = bytes.NewReader(encrypted) + return io.ReadAll(c.decrypter) +} + +func (c *StreamCryptor) GetKey() string { + b64 := base64.RawStdEncoding + return fmt.Sprintf("%s.%s.%s.%s", + b64.EncodeToString(c.enckey), + b64.EncodeToString(c.macKey), + b64.EncodeToString(c.encrypter.IV), + b64.EncodeToString(c.encrypter.Mac.Sum(nil)), + ) +} + +func writeHash(mac hash.Hash, p []byte) error { + m, err := mac.Write(p) + if err != nil { + return err + } + if m != len(p) { + return errors.New("could not write all bytes to hmac") + } + return nil +} diff --git a/plugin/crypto/pkg/method/xor.go b/plugin/crypto/pkg/method/xor.go new file mode 100755 index 0000000..fc764e2 --- /dev/null +++ b/plugin/crypto/pkg/method/xor.go @@ -0,0 +1,100 @@ +package method + +import ( + "crypto/subtle" + "encoding/base64" + "errors" +) + +// SimpleXorCryptor 加密一次 +type SimpleXorCryptor struct { + key []byte +} + +func newSimpleXor(cfg Key) (ICryptor, error) { + var cryptor *SimpleXorCryptor + if cfg.Key == "" { + return nil, errors.New("xor cryptor config no key") + } else { + cryptor = &SimpleXorCryptor{key: []byte(cfg.Key)} + } + return cryptor, nil +} + +// simpleXorEncryptDecrypt 对给定的字节数组进行 XOR 加密和解密 +// key 是用于加密和解密的密钥 +func simpleXorEncryptDecrypt(data []byte, key []byte) []byte { + dataLen := len(data) + result := make([]byte, dataLen) + keyLen := len(key) + for i := 0; i < dataLen; i += keyLen { + end := i + keyLen + if end > dataLen { + end = dataLen + } + subtle.XORBytes(result[i:end], data[i:end], key[:end-i]) + } + return result +} + +func (c *SimpleXorCryptor) Encrypt(origin []byte) ([]byte, error) { + return simpleXorEncryptDecrypt(origin, c.key), nil +} + +func (c *SimpleXorCryptor) Decrypt(encrypted []byte) ([]byte, error) { + return simpleXorEncryptDecrypt(encrypted, c.key), nil +} + +func (c *SimpleXorCryptor) GetKey() string { + return base64.RawStdEncoding.EncodeToString(c.key) +} + +// 复杂的XOR加密器 加密两次 +type ComplexXorCryptor struct { + key []byte + iv []byte +} + +func newComplexXor(cfg Key) (ICryptor, error) { + var cryptor *ComplexXorCryptor + if cfg.Key == "" { + return nil, errors.New("xor cryptor config no key") + } else { + cryptor = &ComplexXorCryptor{key: []byte(cfg.Key), iv: []byte(cfg.Iv)} + } + return cryptor, nil +} + +// complexXorEncryptDecrypt 对给定的字节数组进行 XOR 加密和解密 +func complexXorEncryptDecrypt(arrayBuffer, key, iv []byte) []byte { + // Assuming the key and iv have been provided and are not nil + if key == nil || iv == nil { + panic("key and iv must not be nil") + } + + result := make([]byte, len(arrayBuffer)) + keyLen := len(key) + ivLen := len(iv) + + for i := 0; i < len(result); i++ { + result[i] = arrayBuffer[i] ^ (key[i%keyLen] ^ iv[i%ivLen]) + } + return result +} + +func (c *ComplexXorCryptor) Encrypt(origin []byte) ([]byte, error) { + return complexXorEncryptDecrypt(origin, c.key, c.iv), nil +} + +func (c *ComplexXorCryptor) Decrypt(encrypted []byte) ([]byte, error) { + return complexXorEncryptDecrypt(encrypted, c.key, c.iv), nil +} + +func (c *ComplexXorCryptor) GetKey() string { + return base64.RawStdEncoding.EncodeToString(c.key) + "." + base64.RawStdEncoding.EncodeToString(c.iv) +} + +func init() { + RegisterCryptor("xor_s", newSimpleXor) + RegisterCryptor("xor_c", newComplexXor) +} diff --git a/plugin/crypto/pkg/transform.go b/plugin/crypto/pkg/transform.go new file mode 100644 index 0000000..71e0d06 --- /dev/null +++ b/plugin/crypto/pkg/transform.go @@ -0,0 +1,177 @@ +package crypto + +import ( + "github.com/deepch/vdk/codec/h265parser" + "m7s.live/v5/pkg" + "m7s.live/v5/pkg/codec" + "m7s.live/v5/pkg/task" + + "fmt" + + m7s "m7s.live/v5" + "m7s.live/v5/plugin/crypto/pkg/method" +) + +// GlobalConfig 全局加密配置 +var GlobalConfig Config + +type Config struct { + IsStatic bool `desc:"是否静态密钥" default:"false"` + Algo string `desc:"加密算法" default:"aes_ctr"` //加密算法 + EncryptLen int `desc:"加密字节长度" default:"1024"` //加密字节长度 + Secret struct { + Key string `desc:"加密密钥" default:"your key"` //加密密钥 + Iv string `desc:"加密向量" default:"your iv"` //加密向量 + } `desc:"密钥配置"` +} + +type Transform struct { + m7s.DefaultTransformer + cryptor method.ICryptor +} + +func NewTransform() m7s.ITransformer { + ret := &Transform{} + ret.SetDescription(task.OwnerTypeKey, "Crypto") + return ret +} + +// ValidateAndCreateKey 验证并创建加密密钥 +func ValidateAndCreateKey(isStatic bool, algo string, secretKey, secretIv, streamPath string) (keyConf method.Key, err error) { + if isStatic { + switch algo { + case "aes_ctr": + keyConf.Key = secretKey + keyConf.Iv = secretIv + if len(keyConf.Iv) != 16 || len(keyConf.Key) != 32 { + return keyConf, fmt.Errorf("key or iv length is wrong") + } + case "xor_s": + keyConf.Key = secretKey + if len(keyConf.Key) != 32 { + return keyConf, fmt.Errorf("key length is wrong") + } + case "xor_c": + keyConf.Key = secretKey + keyConf.Iv = secretIv + if len(keyConf.Iv) != 16 || len(keyConf.Key) != 32 { + return keyConf, fmt.Errorf("key or iv length is wrong") + } + default: + return keyConf, fmt.Errorf("algo type is wrong") + } + } else { + /* + 动态加密 + key = md5(密钥+流名称) + iv = md5(流名称)前一半 + */ + if secretKey != "" { + keyConf.Key = method.Md5Sum(secretKey + streamPath) + keyConf.Iv = method.Md5Sum(streamPath)[:16] + } else { + return keyConf, fmt.Errorf("secret key is empty") + } + } + return +} + +func (t *Transform) Start() error { + // 在 Start 时获取并保存配置 + t.Info("transform job started") + + keyConf, err := ValidateAndCreateKey(GlobalConfig.IsStatic, GlobalConfig.Algo, GlobalConfig.Secret.Key, GlobalConfig.Secret.Iv, t.TransformJob.StreamPath) + if err != nil { + return err + } + + t.cryptor, err = method.GetCryptor(GlobalConfig.Algo, keyConf) + if err != nil { + t.Error("failed to create cryptor", "error", err) + return err + } + + // 使用 TransformJob 的 Subscribe 方法订阅流 + if err := t.TransformJob.Subscribe(); err != nil { + t.Error("failed to subscribe stream", "error", err) + return err + } + + t.Info("crypto transform started", + "stream", t.TransformJob.StreamPath, + "algo", GlobalConfig.Algo, + "isStatic", GlobalConfig.IsStatic, + ) + + return nil +} + +func (t *Transform) Go() error { + // 创建发布者 + if err := t.TransformJob.Publish(t.TransformJob.StreamPath + "/crypto"); err != nil { + t.Error("failed to create publisher", "error", err) + return err + } + + // 处理音视频流 + return m7s.PlayBlock(t.TransformJob.Subscriber, + func(audio *pkg.RawAudio) (err error) { + copyAudio := &pkg.RawAudio{ + FourCC: audio.FourCC, + Timestamp: audio.Timestamp, + } + audio.Memory.Range(func(b []byte) { + copy(copyAudio.NextN(len(b)), b) + }) + return t.TransformJob.Publisher.WriteAudio(copyAudio) + }, + func(video *pkg.H26xFrame) error { + // 处理视频帧 + if video.GetSize() == 0 { + return nil + } + copyVideo := &pkg.H26xFrame{ + FourCC: video.FourCC, + CTS: video.CTS, + Timestamp: video.Timestamp, + } + + for _, nalu := range video.Nalus { + mem := copyVideo.NextN(nalu.Size) + copy(mem, nalu.ToBytes()) + needEncrypt := false + if video.FourCC == codec.FourCC_H264 { + switch codec.ParseH264NALUType(mem[0]) { + case codec.NALU_Non_IDR_Picture, codec.NALU_IDR_Picture: + needEncrypt = true + } + } else if video.FourCC == codec.FourCC_H265 { + switch codec.ParseH265NALUType(mem[0]) { + case h265parser.NAL_UNIT_CODED_SLICE_BLA_W_LP, + h265parser.NAL_UNIT_CODED_SLICE_BLA_W_RADL, + h265parser.NAL_UNIT_CODED_SLICE_BLA_N_LP, + h265parser.NAL_UNIT_CODED_SLICE_IDR_W_RADL, + h265parser.NAL_UNIT_CODED_SLICE_IDR_N_LP, + h265parser.NAL_UNIT_CODED_SLICE_CRA: + needEncrypt = true + } + } + if needEncrypt { + if encBytes, err := t.cryptor.Encrypt(mem); err == nil { + copyVideo.Nalus.Append(encBytes) + } else { + copyVideo.Nalus.Append(mem) + } + } else { + copyVideo.Nalus.Append(mem) + } + } + return t.TransformJob.Publisher.WriteVideo(copyVideo) + }) +} + +func (t *Transform) Dispose() { + t.Info("crypto transform disposed", + "stream", t.TransformJob.StreamPath, + ) +}