From 763be7adbbf5dd1ded39940c45b06988ebcfd1b9 Mon Sep 17 00:00:00 2001 From: ziming Date: Fri, 10 Oct 2025 18:52:33 +0800 Subject: [PATCH] first commit --- .gitignore | 12 ++ README.md | 246 ++++++++++++++++++++++++++++++++++ api/service.go | 9 ++ api/v1/anyapi/anyapi.go | 41 ++++++ api/v1/anyapi/anyapi_test.go | 80 +++++++++++ api/v1/key/key.go | 125 ++++++++++++++++++ api/v1/key/key_test.go | 150 +++++++++++++++++++++ api/v1/key/models.go | 212 +++++++++++++++++++++++++++++ api/v1/key/vo.go | 45 +++++++ api/v2/key.go | 45 +++++++ api/v2/key_test.go | 130 ++++++++++++++++++ api/v2/models.go | 166 +++++++++++++++++++++++ api/v2/vo.go | 97 ++++++++++++++ cmd/rsa/main.go | 19 +++ cmd/sm/main.go | 20 +++ core/config.go | 90 +++++++++++++ core/core.go | 249 +++++++++++++++++++++++++++++++++++ core/core_test.go | 63 +++++++++ core/interface.go | 30 +++++ core/response.go | 28 ++++ core/rsa.go | 41 ++++++ core/sm.go | 41 ++++++ go.mod | 23 ++++ go.sum | 101 ++++++++++++++ utils/rsa/aes.go | 122 +++++++++++++++++ utils/rsa/aes_test.go | 31 +++++ utils/rsa/cipher.go | 30 +++++ utils/rsa/cipher_test.go | 43 ++++++ utils/rsa/generate.go | 59 +++++++++ utils/rsa/generate_test.go | 29 ++++ utils/rsa/pem.go | 75 +++++++++++ utils/rsa/pem_test.go | 21 +++ utils/rsa/sign.go | 31 +++++ utils/rsa/sign_test.go | 63 +++++++++ utils/sm/cipher.go | 29 ++++ utils/sm/cipher_test.go | 33 +++++ utils/sm/generate.go | 46 +++++++ utils/sm/generate_test.go | 28 ++++ utils/sm/pem.go | 37 ++++++ utils/sm/sign.go | 28 ++++ utils/sm/sign_test.go | 49 +++++++ utils/sm/sm4.go | 77 +++++++++++ utils/sm/sm4_test.go | 53 ++++++++ utils/sort_struct.go | 45 +++++++ utils/sort_struct_test.go | 19 +++ 45 files changed, 3011 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 api/service.go create mode 100644 api/v1/anyapi/anyapi.go create mode 100644 api/v1/anyapi/anyapi_test.go create mode 100644 api/v1/key/key.go create mode 100644 api/v1/key/key_test.go create mode 100644 api/v1/key/models.go create mode 100644 api/v1/key/vo.go create mode 100644 api/v2/key.go create mode 100644 api/v2/key_test.go create mode 100644 api/v2/models.go create mode 100644 api/v2/vo.go create mode 100644 cmd/rsa/main.go create mode 100644 cmd/sm/main.go create mode 100644 core/config.go create mode 100644 core/core.go create mode 100644 core/core_test.go create mode 100644 core/interface.go create mode 100644 core/response.go create mode 100644 core/rsa.go create mode 100644 core/sm.go create mode 100644 go.mod create mode 100644 go.sum create mode 100644 utils/rsa/aes.go create mode 100644 utils/rsa/aes_test.go create mode 100644 utils/rsa/cipher.go create mode 100644 utils/rsa/cipher_test.go create mode 100644 utils/rsa/generate.go create mode 100644 utils/rsa/generate_test.go create mode 100644 utils/rsa/pem.go create mode 100644 utils/rsa/pem_test.go create mode 100644 utils/rsa/sign.go create mode 100644 utils/rsa/sign_test.go create mode 100644 utils/sm/cipher.go create mode 100644 utils/sm/cipher_test.go create mode 100644 utils/sm/generate.go create mode 100644 utils/sm/generate_test.go create mode 100644 utils/sm/pem.go create mode 100644 utils/sm/sign.go create mode 100644 utils/sm/sign_test.go create mode 100644 utils/sm/sm4.go create mode 100644 utils/sm/sm4_test.go create mode 100644 utils/sort_struct.go create mode 100644 utils/sort_struct_test.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5efb8a4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,12 @@ +# Test binary, built with `go test -c` +*.test +# Go workspace file +go.work +go.work.sum +# OS General +Thumbs.db +.DS_Store + +.vscode/ +.idea/ +pem \ No newline at end of file diff --git a/README.md b/README.md new file mode 100644 index 0000000..43e8890 --- /dev/null +++ b/README.md @@ -0,0 +1,246 @@ +## 功能介绍 + +1. 接口 SDK。详见 [接口介绍](services)。 +2. HTTP 客户端,支持请求签名和应答验签。如果 SDK 未支持你需要的接口,请用此客户端发起请求。 +3. 回调通知处理库,支持回调通知的验签。详见 [回调通知验签](#回调通知的验签)。 +4. 密钥生成下载、[敏感信息加解密](#敏感信息加解密) 等辅助能力。 + + +#### 名词解释 ++ **商户 API 公钥**,是用来证实商户身份的 ++ **商户 API 私钥**。是用来证实商户身份的 ++ **商户 API 密钥**。是商户用来加密请求参数的密钥,为加强数据安全,使用的对称加密密钥。 +> :warning: 不要把私钥文件暴露在公共场合,如上传到 Github,写在客户端代码等。 + +## 快速开始 + +### 安装 + +#### 1、使用 Go Modules 管理你的项目 + +如果你的项目还不是使用 Go Modules 做依赖管理,在项目根目录下执行: + +```shell +go mod init +``` + +#### 2、在项目目录中执行: +```shell +go get -u github.com/sleepinggodoflove/lansexiongdi-marketing-sdk +``` +来添加依赖,完成 `go.mod` 修改与 SDK 下载。 + + +## 示例 + +#### [获取key码](https://tvd8jq9lqkp.feishu.cn/wiki/PVq3wtanPicDu0kyfpLc0McMnAc?from=from_copylink) + +```go +package main + +import ( + "context" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/api/v1/key" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/core" + "log" +) + +func main() { + c, err := core.NewCore(&core.Config{ + AppID: "123", + PrivateKey: "私钥", + PublicKey: "验签公钥", + Key: "业务参数密钥key", + SignType: "签名类型", + BaseURL: "请求地址:https://api.lansexiongdi.com", + }) + if err != nil { + log.Fatalf("new core err:%v", err) + } + a := &key.Key{c} + _,_,r, err := a.Order(context.Background(), &key.OrderRequest{ + OutBizNo: "123456", + ActivityNo: "123456", + Number: 1, + }) + if err != nil { + log.Fatalf("key get err:%v", err) + } + log.Printf(r) +} +``` + +#### [查询key码](https://tvd8jq9lqkp.feishu.cn/wiki/GvRswEDyfiXGUUkkDCYc8xg4nVX?from=from_copylink) +```go +package main + +import ( + "context" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/api/v1/key" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/core" + "log" +) + +func main() { + c, err := core.NewCore(&core.Config{ + AppID: "123", + PrivateKey: "私钥", + PublicKey: "验签公钥", + Key: "业务参数密钥key", + SignType: "签名类型", + BaseURL: "请求地址", + }) + if err != nil { + log.Fatalf("new core err:%v", err) + } + a := &key.Key{c} + _,_,r, err := a.Query(context.Background(), &key.QueryRequest{ + OutBizNo: "123456", + trade_no: "123456", + }) + if err != nil { + log.Fatalf("key query err:%v", err) + } + log.Printf(r) +} +``` + +#### [作废key码](https://tvd8jq9lqkp.feishu.cn/wiki/R9NMw96eIiXLiRkOi7icANkynbb?from=from_copylink) +```go +package main + +import ( + "context" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/api/v1/key" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/core" + "log" +) + +func main() { + c, err := core.NewCore(&core.Config{ + AppID: "123", + PrivateKey: "私钥", + PublicKey: "验签公钥", + Key: "业务参数密钥key", + SignType: "签名类型", + BaseURL: "请求地址", + }) + if err != nil { + log.Fatalf("new core err:%v", err) + } + a := &key.Key{c} + _,_,r, err := a.Discard(context.Background(), &key.DiscardRequest{ + OutBizNo: "123456", + trade_no: "123456", + }) + if err != nil { + log.Fatalf("key query err:%v", err) + } + log.Printf(r) +} +``` + +#### [回调通知](https://alidocs.dingtalk.com/i/nodes/N7dx2rn0Jb6A1wvLixErNlLkJMGjLRb3?utm_scene=team_space) +```go +package main + +import ( + "context" + "net/http" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/api/v1/key" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/core" + "log" +) + +func main() { + c, err := core.NewCore(&core.Config{ + AppID: "123", + PrivateKey: "私钥", + PublicKey: "验签公钥", + Key: "业务参数密钥key", + SignType: "签名类型", + BaseURL: "请求地址", + }) + if err != nil { + log.Fatalf("new core err:%v", err) + } + a := &key.Key{c} + + req := &http.Request{ + Header: nil, // 请求头 + Body: nil, // 请求体 + } + r, err := a.CallBack(context.Background(), req) + if err != nil { + log.Fatalf("key callBack err:%v", err) + } + if err != nil { + log.Fatalf("key notify err:%v", err) + } + log.Printf(r) +} +``` + +#### [其它调用](https://alidocs.dingtalk.com/i/nodes/N7dx2rn0Jb6A1wvLixErNlLkJMGjLRb3?utm_scene=team_space) + +```go +package main + +import ( + "context" + "encoding/json" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/api/v1/anyapi" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/core" + "log" +) + +func main() { + c, err := core.NewCore(&core.Config{ + AppID: "appid", + PrivateKey: "私钥", + PublicKey: "验签公钥", + Key: "业务参数密钥key", + SignType: "签名类型", + BaseURL: "请求地址", + }) + if err != nil { + log.Fatalf("new core err:%v", err) + } + + bizContent := struct { + Source string `json:"source"` // 来源 + AppId string `json:"app_id"` // 应用Id + MchPublicKey string `json:"mch_public_key"` // 客户公钥 + NotifyUrl string `json:"notify_url"` // 事件通知地址,可为空 + }{ + Source: "来源", + AppId: "123", + MchPublicKey: "123", + NotifyUrl: "https://xx.com/xx", + } + + a := &anyapi.AnyApi{c} + + method := "/openapi/v1/xxx" + + _,_, r, err := a.AnyApi(context.Background(), method, bizContent) + if err != nil { + log.Fatalf("call err:%v", err) + } + + if !r.IsSuccess() { + log.Fatalf("err:%s", r.Message) + } + + var bizDataContent = struct { + Ciphertext string `json:"ciphertext,omitempty"` + }{} + + _ = json.Unmarshal(r.Data, &bizDataContent) + + bizJsonContent, _ := c.CryptographySuite.Cipher.Decode(bizDataContent.Ciphertext) + + log.Printf("bizJsonContent=%s", bizJsonContent) +} + +``` \ No newline at end of file diff --git a/api/service.go b/api/service.go new file mode 100644 index 0000000..1cf2011 --- /dev/null +++ b/api/service.go @@ -0,0 +1,9 @@ +package api + +import ( + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/core" +) + +type Service struct { + *core.Core +} diff --git a/api/v1/anyapi/anyapi.go b/api/v1/anyapi/anyapi.go new file mode 100644 index 0000000..d0d8ea2 --- /dev/null +++ b/api/v1/anyapi/anyapi.go @@ -0,0 +1,41 @@ +package anyapi + +import ( + "context" + "encoding/json" + "fmt" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/api" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/core" + "net/http" +) + +type AnyApi api.Service + +func (a *AnyApi) AnyApi(ctx context.Context, method string, bizContent any) (http.Header, *http.Response, *core.Response, error) { + + p, err := a.BuildAnyApiParams(bizContent) + if err != nil { + return nil, nil, nil, err + } + + reqBodyBytes, err := json.Marshal(p) + if err != nil { + return nil, nil, nil, err + } + + h := a.GetHeaders(p) + + url := fmt.Sprintf("%s%s", a.Config.BaseURL, method) + + httpResponse, bodyBytes, err := a.Request(ctx, h, http.MethodPost, url, reqBodyBytes) + if err != nil { + return nil, nil, nil, err + } + + res, err := core.BuildResponse(bodyBytes) + if err != nil { + return h, httpResponse, nil, err + } + + return h, httpResponse, res, nil +} diff --git a/api/v1/anyapi/anyapi_test.go b/api/v1/anyapi/anyapi_test.go new file mode 100644 index 0000000..1c9271a --- /dev/null +++ b/api/v1/anyapi/anyapi_test.go @@ -0,0 +1,80 @@ +package anyapi + +import ( + "context" + "encoding/json" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/core" + "testing" +) + +var ( + appId = "OP001" + privateKey = "" + publicKey = "" + key = "" + baseURL = "http://127.0.0.1:9000" + signType = core.SignRSA +) + +func newCore() *core.Core { + + c, err := core.NewCore(&core.Config{ + AppID: appId, + PrivateKey: privateKey, + PublicKey: publicKey, + Key: key, + SignType: signType, + BaseURL: baseURL, + }) + + if err != nil { + panic(err) + } + + return c +} + +func Test_AnyApi(t *testing.T) { + + c := newCore() + + bizContent := struct { + Source string `json:"source"` // 来源 + AppId string `json:"app_id"` // 应用Id + MchPublicKey string `json:"mch_public_key"` // 客户公钥 + NotifyUrl string `json:"notify_url"` // 事件通知地址,可为空 + }{ + Source: "来源", + AppId: "OP002", + MchPublicKey: "", + NotifyUrl: "https://utils.85938.cn/utils/v1/wechat/notify", + } + + a := &AnyApi{c} + + method := "/openapi/v1/merchant/appSet" + + h, _, r, err := a.AnyApi(context.Background(), method, bizContent) + if err != nil { + t.Error(err) + return + } + + if !r.IsSuccess() { + t.Error(r.Message) + return + } + + t.Logf("data=%s", string(r.Data)) + + var bizDataContent = struct { + Ciphertext string `json:"ciphertext,omitempty"` + }{} + + _ = json.Unmarshal(r.Data, &bizDataContent) + + bizJsonContent, _ := c.CryptographySuite.Cipher.Decode(bizDataContent.Ciphertext) + + t.Logf("header=%+v", h) + t.Logf("bizJsonContent=%s", bizJsonContent) +} diff --git a/api/v1/key/key.go b/api/v1/key/key.go new file mode 100644 index 0000000..8a320f9 --- /dev/null +++ b/api/v1/key/key.go @@ -0,0 +1,125 @@ +package key + +import ( + "context" + "encoding/json" + "fmt" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/api" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/core" + "io" + "net/http" +) + +const ( + orderMethod = "/openapi/v1/key/order" + queryMethod = "/openapi/v1/key/query" + discardMethod = "/openapi/v1/key/discard" +) + +type Key api.Service + +func (k *Key) Order(ctx context.Context, request *OrderRequest) (http.Header, *http.Response, *core.Response, error) { + + h, httpResponse, bodyBytes, err := k.Post(ctx, orderMethod, request) + if err != nil { + return nil, nil, nil, err + } + + res, err := core.BuildResponse(bodyBytes) + if err != nil { + return h, httpResponse, nil, err + } + + return h, httpResponse, res, nil +} + +func (k *Key) Query(ctx context.Context, request *QueryRequest) (http.Header, *http.Response, *core.Response, error) { + + h, httpResponse, bodyBytes, err := k.Post(ctx, queryMethod, request) + if err != nil { + return nil, nil, nil, err + } + + res, err := core.BuildResponse(bodyBytes) + if err != nil { + return h, httpResponse, nil, err + } + + return h, httpResponse, res, nil +} + +func (k *Key) Discard(ctx context.Context, request *DiscardRequest) (http.Header, *http.Response, *core.Response, error) { + + h, httpResponse, bodyBytes, err := k.Post(ctx, discardMethod, request) + if err != nil { + return nil, nil, nil, err + } + + res, err := core.BuildResponse(bodyBytes) + if err != nil { + return h, httpResponse, nil, err + } + + return h, httpResponse, res, nil +} + +func (k *Key) Notify(_ context.Context, n *Notify) (*NotifyData, error) { + + if !k.CryptographySuite.Verifier.Verify(n.SignString(), n.Sign) { + return nil, fmt.Errorf("verify sign fail") + } + + return &n.Data, nil +} + +func (k *Key) CallBack(ctx context.Context, req *http.Request) (*NotifyData, error) { + + body, err := io.ReadAll(req.Body) + if err != nil { + return nil, err + } + + var n *Notify + if err = json.Unmarshal(body, &n); err != nil { + return nil, err + } + + sign := req.Header.Get("Sign") + + if sign == "" { + return k.Notify(ctx, n) + } + + timestamp := req.Header.Get("Timestamp") + if timestamp == "" { + return nil, fmt.Errorf("timestamp is empty") + } + + appid := req.Header.Get("Appid") + if appid == "" { + return nil, fmt.Errorf("appid is empty") + } + + signType := req.Header.Get("Sign-Type") + if signType == "" { + return nil, fmt.Errorf("sign-type is empty") + } + + if appid != k.Config.AppID { + return nil, fmt.Errorf("appid is invalid") + } + if signType != string(k.Config.SignType) { + return nil, fmt.Errorf("sign-type is invalid") + } + + ciphertext, err := k.GetCiphertext(&n.Data) + if err != nil { + return nil, err + } + + if !k.Verify(timestamp, ciphertext, sign) { + return nil, fmt.Errorf("call back verify sign fail") + } + + return &n.Data, nil +} diff --git a/api/v1/key/key_test.go b/api/v1/key/key_test.go new file mode 100644 index 0000000..c25ba7e --- /dev/null +++ b/api/v1/key/key_test.go @@ -0,0 +1,150 @@ +package key + +import ( + "bytes" + "context" + "encoding/json" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/core" + "io" + "net/http" + "testing" +) + +var ( + appId = "lzm" + privateKey = "MIIEowIBAAKCAQEA7a2I4l8OOdW4weVFvj4u/mBqP3aZhJ0mOTKl4MCW4Pf6gNAlZa5dZYOS/BocmG872+pd10BiI73qiAWsuVaPwCL0A37lQbCXlG0fDAfCLogXuF1qVNRZgkYKrx/5Gppo2PNed7E5YyCUkMUKVPbuwuZteMZJH8d1o6Uojbb/xJQvAGOlx5Y04VZWp/6p2GjhW0srwgbpVegMyyn2Qblx1Lo+Uq5zG8um7FTpbtb/L/itpBFEDSZGIIKDfn4FPyt+jQ0SW5TDYQClSvWHK4V3RkWOVkD1nHeBpZyp7JNehK+7kBfO6G4NJabkyoWqFEiZcTy38ZWQdqJ9N4LZuY37NwIDAQABAoIBAGs2u6e5z1YBda1pehN+Q36WCXeFTW0H4qUslq0S0zy6P/L5cdUzWYggWR6FvN56Vts2Foyxy1NqKTCgtrCIPqIiYkZtaIdAXLAkpTutCEgrNeABq6SGgbYFWG51Es6QVrl+1t9RP5zaponDiIyZM00R2tH/SB8gv41JREjhAvEuNIwPyaoVVt+U/kAdhJgiMKsDpoGaMfsJk76sORu6qQqBkBN8cglN94xC0QtROytW3EY8SnZmgGZHcY3YTXM74CWM8yBg7rNuKv0982f9hKvUDHKMFYly1PzYiSgplkT7RYCMjo2FFf1lt7k7N61+4nalS/EM6324m2poisTRFAkCgYEA+3oXvtwrTim0kXG7V7w5PS8u5dU0sAAH4ACSyzy8nEdKEk4ipoaTGm6km8ko0O+9E70SwZQgK/eAnmsYf5WtMzvItweKUUVsBCm01qSHlu5vzGO3H1ndi7hg+tH9VrOQH3+odQJP9FqC4BkncMszHM4nLglWSTixTTvGIovQLy0CgYEA8fPnu0tWqhfQV6svaA5kt4h6cL52ARKlubRuYkI4hGuikKYpd2A3WuVtD1LkuPQSjwID9730HAqLc7ZMwONjQ8NANi9ZoJR6A+Vzba9zDPQmSc80Ax7Kkjc03D1Y7yiP6P8bWnhCCbRcMy+dcobvBZc2zaWzSNjZwPOSV9xaMnMCgYAclz354hg+U7mGy7JsACdV0HZ5hOrvk6FRk18dIjOjZOuD90QzQJua5rdqSs2MK6WIh/eI8KlTtlj2KeDoKIE/kO15+a59HPJx6rf3q08LFuK5DyEzvEjW6MiF27f80n9xRVdGrlOeyWeVyOZWCZQvEzUbI86eloZ57HDTXqf1pQKBgF6T6xeJgZ0Hpgc/AU75oWEk1kfQC6yrr2CCKUv7esA4mtlUOo1RbRH48MK2snWh4sdIEGj9NbjoXk6jCim0OQ85+ZW0uKJOp8tyG8baeGyt23GqrzgxBxpUvjMBQAxsnKSFZBnfPGEywX+4syEbob9btq54gTaOncAQ9jmmBxQFAoGBAIpPbq2lYwOhgoUJ2BR34xjpmNOiOAF5AVLPGTH44a+iGMJ4tbF9AvfL4xsCWK9zMi3ExaKVN0lNn0cWx2lpXxwO6B+l4L//eczmHx4h1eLJd6ZWyTj7lq+RBOOUgHLKEssZfJ11RYTZjSD7s75JZteM2OFw7BVRRNgw387A3mj6" + publicKey = "MIIBCgKCAQEA0w4XGS1eEO9gAtWoB0E1vi1QH3xZAiHnkzZMhZRJOKeZhNUb9nmPzrGtCFD1c+to9/hxKnWZnRi1dklRGI4uXaB7PKuDhifHarBTTPBzW/8m+YqKEwjT2XWYvnG1Zeek4a45xze5cHhLA7Ow1Lwgy0u1rhalvz8GbCa9A7ZHKvZtIJJzfPSIV6gZIz5b7+v7rXZzMNNxvC7m+cwtvvERPjhJoj3O7ithcgdiT3JkZd1fZxkA6HCJx1I+TElt4qA9WnV+rqQwjka1gxBO497c0MUq+4Tx+lLGKpb61RPja4+9wiLFvEiS80WyZYkptWlA0Z5mhsxURs/OqaMyVXzbhwIDAQAB" + key = "5f42e758a38cc003c5da7cee814ddfd5" + baseURL = "https://gateway.dev.cdlsxd.cn" + signType = core.SignRSA +) + +func newCore() *Key { + c, _ := core.NewCore(&core.Config{ + AppID: appId, + PrivateKey: privateKey, + PublicKey: publicKey, + Key: key, + SignType: signType, + BaseURL: baseURL, + }) + k := Key{c} + + return &k +} + +func TestOrder(t *testing.T) { + + c := newCore() + + h, _, r, err := c.Order(context.Background(), &OrderRequest{ + OutBizNo: "cs002", + ActivityNo: "lzmcs", + Number: 1, + NotifyUrl: "", + Account: "", + Extra: "", + }) + if err != nil { + t.Error(err) + return + } + + if !r.IsSuccess() { + t.Errorf("获取key失败:%s", r.Message) + return + } + + t.Logf("data=%s", string(r.Data)) + t.Logf("headers=%+v", h) +} + +func TestQuery(t *testing.T) { + + c := newCore() + + h, _, r, err := c.Query(context.Background(), &QueryRequest{ + OutBizNo: "cs001", + TradeNo: "", + }) + if err != nil { + t.Error(err) + return + } + + if !r.IsSuccess() { + t.Errorf("查询失败:%s", r.Message) + return + } + + t.Logf("data=%s", string(r.Data)) + t.Logf("headers=%+v", h) + //t.Log(result.Status.IsNormal()) +} + +func TestDiscard(t *testing.T) { + + c := newCore() + + h, _, r, err := c.Discard(context.Background(), &DiscardRequest{ + OutBizNo: "N123456003", + TradeNo: "", + Reason: "正常作废", + }) + if err != nil { + t.Error(err) + return + } + + if !r.IsSuccess() { + t.Errorf(r.Message) + return + } + + t.Logf("headers=%+v", h) + t.Logf("respons=%+v", r) +} + +func TestResponse(t *testing.T) { + + jsonBytes := []byte(`{"code":200,"data":{},"message":"成功"}`) + resp, err := core.BuildResponse(jsonBytes) + if err != nil { + t.Error(err) + return + } + + result, err := ConvertData(resp.Data) + if err != nil { + t.Error(err) + return + } + + t.Logf("%+v", result) +} + +func TestCallBack(t *testing.T) { + + reqStr := `{"data": {"url": "https://gateway.dev.cdlsxd.cn/yxh5/dpK5ly6oVVE2AM0W", "status": 2, "account": "18479266021", "trade_no": "794167617429315585", "notify_id": "7345294954732199936", "usage_num": 0, "out_biz_no": "0627002001", "usable_num": 2, "usage_time": "2025-06-30 11:39:46", "valid_end_time": "2026-06-30 23:59:59", "settlement_price": 2, "valid_begin_time": "2025-06-24 11:00:08"}, "sign": "XLwRQ12EBXSGOSVzUMXwjSlKP88P4Odhe6c9MrfaszKLe+3HtPTeB6QWvyAmXGeIvsy02P0YtcOYV4xQHlWo3Uh5FZc6IJU/+KN+xVnn/DlFLpc+DhCKw6o4hYv+eLLyshjFZPZYVUU2I2YmkI1ZlwBaufsB+N9ds8gBz5+hELn17/qcFcbO6pYOd2te7xmJSGKOAMn0q2c2DSvTLvyQXhKUlDZfUZZGBOc1LGChy9CHc7Z/0E8/p2YYTlMPnvk0VHjEjV5sJxDnXwhSZqE7f3mRx0IN3au3VtZnXJsgl/whxdTyab9dYpfIxK75bS0mjncdqxGf1hLdhYJhTx8bog==", "app_id": "lzm", "sign_type": "RSA", "timestamp": "2025-06-30 11:39:46"}` + headerStr := `{"Sign": ["Boj6IrOOrRATJt0IBE+z5Ie/g4mo3MZk+JpJ4bLYoBbDfMqvgTBhxqiC8CheRm/nEF9iFFJCvq9S0dL25fLexQ1k5AxE3cX1+qR5fCRdaiZvqWG4jaXOjUUW8K7fQ9g5ii6T4b3cWp71FBHiG3ZH5XohM9JuLo3W17MxrizsLLD0euGROAY3bXcakVustto07V3i0g59+ajsCTTdxF/gNcrsO5a3eTJ8CTSDnMgpwqMbU+E9YMX1zGFH/+m/RtL9s8tLRf8j4T/t8g6b94JfvBv+Fu1wV4eMUO7H4Iv0LJ1TL8qMBkWul5BbwGxSdEGQoWU0CIAehYTfR5meKxTOTQ=="], "Appid": ["lzm"], "Version": ["1.0"], "Sign-Type": ["RSA"], "Timestamp": ["2025-06-30 11:39:46"], "Content-Type": ["application/json"]}` + + var headers http.Header + if err := json.Unmarshal([]byte(headerStr), &headers); err != nil { + t.Error(err) + return + } + + c := newCore() + request := &http.Request{ + Header: headers, + Body: io.NopCloser(bytes.NewBuffer([]byte(reqStr))), + } + + r, err := c.CallBack(context.Background(), request) + if err != nil { + t.Error(err) + return + } + + t.Logf("response=%+v", r) +} diff --git a/api/v1/key/models.go b/api/v1/key/models.go new file mode 100644 index 0000000..e05605c --- /dev/null +++ b/api/v1/key/models.go @@ -0,0 +1,212 @@ +package key + +import ( + "encoding/json" + "fmt" + "github.com/go-playground/validator/v10" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/core" +) + +var _ core.Request = (*OrderRequest)(nil) +var _ core.Request = (*DiscardRequest)(nil) +var _ core.Request = (*QueryRequest)(nil) +var _ core.Request = (*NotifyData)(nil) + +type OrderRequest struct { + OutBizNo string `validate:"required,alphanum,min=2,max=32" json:"out_biz_no"` // 同一商户应用下不可重复 + ActivityNo string `validate:"required,min=2,max=32" json:"activity_no"` + Number int32 `validate:"required,eq=1" json:"number"` // v1只支持1,若要多个,请异步v2接口批量生产 + NotifyUrl string `json:"notify_url,omitempty"` // 回调地址,可为空 + Account string `json:"account,omitempty"` // 可兑换账号 + Extra string `json:"extra,omitempty"` // 拓展参数,备用 +} + +func (a *OrderRequest) String() (string, error) { + + b, err := json.Marshal(a) + if err != nil { + return "", err + } + + return string(b), nil +} + +func (c *OrderRequest) Validate() error { + + if err := validator.New().Struct(c); err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf(err.Error()) + } + } + + return nil +} + +type QueryRequest struct { + OutBizNo string `json:"out_biz_no,omitempty" validate:"omitempty,alphanum,min=2,max=32"` // out_biz_no/trade_no二选一 同一商户应用下不可重复 + TradeNo string `json:"trade_no,omitempty" validate:"omitempty,alphanum,min=2,max=32"` // out_biz_no/trade_no二选一 若不为空,则优先使用 +} + +func (a *QueryRequest) String() (string, error) { + + b, err := json.Marshal(a) + if err != nil { + return "", err + } + + return string(b), nil +} + +func (q *QueryRequest) Validate() error { + + if q.OutBizNo == "" && q.TradeNo == "" { + return fmt.Errorf("参数错误,out_biz_no/trade_no 二选一") + } + + if err := validator.New().Struct(q); err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf(err.Error()) + } + } + + return nil +} + +type DiscardRequest struct { + OutBizNo string `json:"out_biz_no,omitempty" validate:"omitempty,alphanum,min=2,max=32"` // out_biz_no/trade_no二选一 同一商户应用下不可重复 + TradeNo string `json:"trade_no,omitempty" validate:"omitempty,alphanum,min=2,max=32"` // out_biz_no/trade_no二选一 若不为空,则优先使用 + Reason string `json:"reason,omitempty" validate:"omitempty,min=1,max=50"` // 可为空 +} + +func (a *DiscardRequest) String() (string, error) { + + b, err := json.Marshal(a) + if err != nil { + return "", err + } + + return string(b), nil +} + +func (d *DiscardRequest) Validate() error { + + if d.OutBizNo == "" && d.TradeNo == "" { + return fmt.Errorf("out_biz_no/trade_no 二选一") + } + + if err := validator.New().Struct(d); err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf(err.Error()) + } + } + + return nil +} + +type NotifyData struct { + NotifyId string `json:"notify_id" validate:"required,alphanum,min=2,max=32"` + OutBizNo string `json:"out_biz_no" validate:"required,alphanum,min=2,max=32"` + TradeNo string `json:"trade_no" validate:"required,alphanum,min=2,max=32"` + Key string `json:"key,omitempty"` + UsableNum uint32 `json:"usable_num"` + UsageNum uint32 `json:"usage_num"` + Status Status `json:"status" validate:"required"` + Url string `json:"url,omitempty"` + Amount float32 `json:"amount,omitempty"` + PayAmount float32 `json:"pay_amount,omitempty"` + PayTime string `json:"pay_time,omitempty"` + SettlementPrice float32 `json:"settlement_price,omitempty"` + ValidBeginTime string `json:"valid_begin_time,omitempty"` + ValidEndTime string `json:"valid_end_time,omitempty"` + UsageTime string `json:"usage_time,omitempty"` + DiscardTime string `json:"discard_time,omitempty"` + Account string `json:"account,omitempty"` // 可兑换账号 +} +type Notify struct { + AppId string `json:"app_id" validate:"required"` + SignType string `json:"sign_type" validate:"required"` + Timestamp string `json:"timestamp" validate:"required"` + Sign string `json:"sign" validate:"required"` + Data NotifyData `json:"data"` +} + +func (d *NotifyData) Validate() error { + + if err := validator.New().Struct(d); err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf(err.Error()) + } + } + + return nil +} + +func (a *NotifyData) String() (string, error) { + + b, err := json.Marshal(a) + if err != nil { + return "", err + } + + return string(b), nil +} + +func (d *Notify) Validate() error { + + if err := validator.New().Struct(d); err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf(err.Error()) + } + } + + return nil +} + +func (a *Notify) String() string { + + b, err := json.Marshal(a) + if err != nil { + return "" + } + + return string(b) +} + +func (a *Notify) SignString() string { + + b, err := json.Marshal(a.Data) + if err != nil { + return "" + } + + return a.AppId + a.Timestamp + string(b) +} + +type Data struct { + OutBizNo string `json:"out_biz_no"` + TradeNo string `json:"trade_no"` + Key string `json:"key"` + UsableNum uint32 `json:"usable_num"` + UsageNum uint32 `json:"usage_num"` + Status Status `json:"status"` + Url string `json:"url"` + Amount float32 `json:"amount,omitempty"` + PayAmount float32 `json:"pay_amount,omitempty"` + PayTime string `json:"pay_time,omitempty"` + SettlementPrice float32 `json:"settlement_price,omitempty"` + ValidBeginTime string `json:"valid_begin_time,omitempty"` + ValidEndTime string `json:"valid_end_time,omitempty"` + UsageTime string `json:"usage_time,omitempty"` + DiscardTime string `json:"discard_time,omitempty"` + Account string `json:"account,omitempty"` // 可兑换账号 +} + +func ConvertData(b []byte) (*Data, error) { + + var data *Data + if err := json.Unmarshal(b, &data); err != nil { + return nil, err + } + + return data, nil +} diff --git a/api/v1/key/vo.go b/api/v1/key/vo.go new file mode 100644 index 0000000..f52493b --- /dev/null +++ b/api/v1/key/vo.go @@ -0,0 +1,45 @@ +package key + +type Status uint8 + +const ( + Normal Status = iota + 1 + Used + Discard + Expire +) + +var statusMap = map[Status]string{ + Normal: "正常", + Used: "已核销", + Discard: "已作废", + Expire: "已过期", +} + +func (s Status) Value() uint8 { + return uint8(s) +} + +func (s Status) GetText() string { + t, ok := statusMap[s] + if !ok { + return "" + } + return t +} + +func (s Status) IsNormal() bool { + return s == Normal +} + +func (s Status) IsUsed() bool { + return s == Used +} + +func (s Status) IsDiscard() bool { + return s == Discard +} + +func (s Status) IsExpire() bool { + return s == Expire +} diff --git a/api/v2/key.go b/api/v2/key.go new file mode 100644 index 0000000..44f8d15 --- /dev/null +++ b/api/v2/key.go @@ -0,0 +1,45 @@ +package v2 + +import ( + "context" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/api" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/core" + "net/http" +) + +const ( + orderMethod = "/openapi/v2/key/order" + queryMethod = "/openapi/v2/key/query" +) + +type Key api.Service + +func (k *Key) Order(ctx context.Context, request *OrderRequest) (http.Header, *http.Response, *core.Response, error) { + + h, httpResponse, bodyBytes, err := k.Post(ctx, orderMethod, request) + if err != nil { + return nil, nil, nil, err + } + + res, err := core.BuildResponse(bodyBytes) + if err != nil { + return h, httpResponse, nil, err + } + + return h, httpResponse, res, nil +} + +func (k *Key) Query(ctx context.Context, request *QueryRequest) (http.Header, *http.Response, *core.Response, error) { + + h, httpResponse, bodyBytes, err := k.Post(ctx, queryMethod, request) + if err != nil { + return nil, nil, nil, err + } + + res, err := core.BuildResponse(bodyBytes) + if err != nil { + return h, httpResponse, nil, err + } + + return h, httpResponse, res, nil +} diff --git a/api/v2/key_test.go b/api/v2/key_test.go new file mode 100644 index 0000000..b991ebe --- /dev/null +++ b/api/v2/key_test.go @@ -0,0 +1,130 @@ +package v2 + +import ( + "context" + "encoding/json" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/core" + "testing" +) + +var ( + appId = "" + privateKey = "" + publicKey = "" + key = "" + baseURL = "" + signType = core.SignRSA +) + +func newCore() (*core.Core, error) { + return core.NewCore(&core.Config{ + AppID: appId, + PrivateKey: privateKey, + PublicKey: publicKey, + Key: key, + SignType: signType, + BaseURL: baseURL, + }) +} + +func TestOrder(t *testing.T) { + c, err := newCore() + if err != nil { + t.Error(err) + return + } + a := &Key{c} + _, r, err := a.Order(context.Background(), &OrderRequest{ + OutBizNo: "b202412270z8q7r1f704", + ActivityNo: "2024070901134", + Number: 1, + }) + if err != nil { + t.Error(err) + return + } + t.Logf("response=%+v", r) + if !r.IsSuccess() { + t.Errorf("获取key失败:%s", r.Message) + return + } +} + +func TestQuery(t *testing.T) { + c, err := newCore() + if err != nil { + t.Error(err) + return + } + a := &Key{c} + _, r, err := a.Query(context.Background(), &QueryRequest{ + OutBizNo: "006", + TradeNo: "", + }) + if err != nil { + t.Error(err) + return + } + t.Logf("response=%+v", r) + if !r.IsSuccess() { + t.Errorf("查询失败:%s", r.Message) + return + } +} + +func TestNotify(t *testing.T) { + c, err := newCore() + if err != nil { + t.Error(err) + return + } + + n := &Notify{ + AppId: "", + SignType: "", + Timestamp: "", + Sign: "", + Data: NotifyData{ + Event: "", + NotifyId: "", + OutBizNo: "", + TradeNo: "", + ActivityNo: "", + Number: 0, + Status: 0, + KeyMapCiphertext: "", + }, + } + + str, err := n.SignString() + if err != nil { + t.Error(err) + return + } + + sign, err := c.CryptographySuite.Signer.Sign(str) + if err != nil { + t.Error(err) + return + } + + n.Sign = sign + + b := c.CryptographySuite.Verifier.Verify(str, sign) + if !b { + t.Error("验签失败") + return + } + + keyMapDecode, err := c.CryptographySuite.Cipher.Decode(n.Data.KeyMapCiphertext) + if err != nil { + t.Error(err) + return + } + + keyMap := make([]*KeyInfo, 0, n.Data.Number) + if err = json.Unmarshal([]byte(keyMapDecode), &keyMap); err != nil { + t.Error(err) + return + } +} diff --git a/api/v2/models.go b/api/v2/models.go new file mode 100644 index 0000000..79bc96a --- /dev/null +++ b/api/v2/models.go @@ -0,0 +1,166 @@ +package v2 + +import ( + "encoding/json" + "fmt" + "github.com/go-playground/validator/v10" +) + +type OrderRequest struct { + OutBizNo string `validate:"required,alphanum,min=2,max=32" json:"out_biz_no"` // 同一商户应用下不可重复 + ActivityNo string `validate:"required,min=2,max=32" json:"activity_no"` + Number int32 `validate:"required,min=1,max=10000" json:"number"` + NotifyUrl string `json:"notify_url,omitempty"` // 回调地址,为空则使用客户应用设置地址 + Extra string `json:"extra,omitempty"` // 拓展参数,备用 +} + +type OrderResponse struct { + OutBizNo string `json:"out_biz_no"` + TradeNo string `json:"trade_no"` +} + +func (a *OrderRequest) String() (string, error) { + + b, err := json.Marshal(a) + if err != nil { + return "", err + } + + return string(b), nil +} + +func (c *OrderRequest) Validate() error { + + if err := validator.New().Struct(c); err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf(err.Error()) + } + } + + return nil +} + +type QueryRequest struct { + OutBizNo string `json:"out_biz_no,omitempty" validate:"omitempty,alphanum,min=2,max=32"` // out_biz_no/trade_no二选一 同一商户应用下不可重复 + TradeNo string `json:"trade_no,omitempty" validate:"omitempty,alphanum,min=2,max=32"` // out_biz_no/trade_no二选一 若不为空,则优先使用 +} + +type QueryResponse struct { + OutBizNo string `json:"out_biz_no"` + TradeNo string `json:"trade_no"` + Status Status `json:"status"` + Number int32 `json:"number"` + KeyMapCiphertext string `json:"ciphertext" validate:"required"` +} + +type KeyInfo struct { + Key string `json:"key,omitempty"` // key码 + Url string `json:"url,omitempty"` // 短链接 + UsableNum uint32 `json:"usable_num"` // 可兑换次数 + UsageNum uint32 `json:"usage_num"` // 已核销次数 + Status KeyStatus `json:"status"` // 状态 + BeginTime string `json:"begin_time"` // 开始时间 + EndTime string `json:"end_time"` // 结束时间 + Amount float32 `json:"amount,omitempty"` // 金额 + PayAmount float32 `json:"pay_amount,omitempty"` // 支付金额 + PayTime string `json:"pay_time,omitempty"` // 支付时间 + SettlementPrice float32 `json:"settlement_price,omitempty"` // 结算价 + UsageTime string `json:"usage_time,omitempty"` // 最后一次核销时间 + DiscardTime string `json:"discard_time,omitempty"` // 作废时间 +} + +func (a *QueryRequest) String() (string, error) { + + b, err := json.Marshal(a) + if err != nil { + return "", err + } + + return string(b), nil +} + +func (q *QueryRequest) Validate() error { + + if q.OutBizNo == "" && q.TradeNo == "" { + return fmt.Errorf("参数错误,out_biz_no/trade_no 二选一") + } + + if err := validator.New().Struct(q); err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf(err.Error()) + } + } + + return nil +} + +type Notify struct { + AppId string `json:"app_id" validate:"required"` + SignType string `json:"sign_type" validate:"required"` + Timestamp string `json:"timestamp" validate:"required"` + Sign string `json:"sign" validate:"required"` + Data NotifyData `json:"data" validate:"required"` +} + +type NotifyData struct { + Event NotifyEvent `json:"event" validate:"required"` + NotifyId string `json:"notify_id" validate:"required"` + OutBizNo string `json:"out_biz_no" validate:"required"` + TradeNo string `json:"trade_no" validate:"required"` + ActivityNo string `json:"activity_no" validate:"required"` + Number int32 `json:"number" validate:"required"` + Status Status `json:"status" validate:"required"` + KeyMapCiphertext string `json:"ciphertext" validate:"required"` +} + +func (d *Notify) Validate() error { + + if err := validator.New().Struct(d); err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf(err.Error()) + } + } + + return nil +} + +func (a *Notify) String() (string, error) { + + b, err := json.Marshal(a) + if err != nil { + return "", err + } + + return string(b), nil +} + +func (d *NotifyData) Validate() error { + + if err := validator.New().Struct(d); err != nil { + for _, err = range err.(validator.ValidationErrors) { + return fmt.Errorf(err.Error()) + } + } + + return nil +} + +func (a *NotifyData) String() (string, error) { + + b, err := json.Marshal(a) + if err != nil { + return "", err + } + + return string(b), nil +} + +func (a *Notify) SignString() (string, error) { + + b, err := a.Data.String() + if err != nil { + return "", nil + } + + return a.AppId + a.Timestamp + b, nil +} diff --git a/api/v2/vo.go b/api/v2/vo.go new file mode 100644 index 0000000..b09c38b --- /dev/null +++ b/api/v2/vo.go @@ -0,0 +1,97 @@ +package v2 + +const SuccessCode = 200 + +type Status uint8 + +const ( + StatusIng Status = iota + 1 + StatusSuccess + StatusFailed +) + +var statusMap = map[Status]string{ + StatusIng: "生成中", + StatusSuccess: "生成完成", + StatusFailed: "生成失败", +} + +func (s Status) Value() uint8 { + return uint8(s) +} + +func (s Status) GetText() string { + t, ok := statusMap[s] + if !ok { + return "" + } + return t +} + +type NotifyEvent string + +const ( + NotifyEventKeyCreate NotifyEvent = "key.create" + NotifyEventKeyUsage NotifyEvent = "key.usage" + NotifyEventKeyDiscard NotifyEvent = "key.discard" +) + +func (s NotifyEvent) Value() string { + return string(s) +} + +func (s NotifyEvent) IsKeyCreate() bool { + return NotifyEventKeyCreate == s +} + +func (s NotifyEvent) IsKeyUsage() bool { + return NotifyEventKeyUsage == s +} + +func (s NotifyEvent) IsKeyDiscard() bool { + return NotifyEventKeyDiscard == s +} + +type KeyStatus uint8 + +const ( + KeyNormal KeyStatus = iota + 1 + KeyUsed + KeyDiscard + KeyExpire +) + +var keyStatusMap = map[KeyStatus]string{ + KeyNormal: "正常", + KeyUsed: "已核销", + KeyDiscard: "已作废", + KeyExpire: "已过期", +} + +func (s KeyStatus) Value() uint8 { + return uint8(s) +} + +func (s KeyStatus) GetText() string { + t, ok := keyStatusMap[s] + if !ok { + return "" + } + return t +} + +func (s KeyStatus) IsNormal() bool { + return s == KeyNormal +} + +func (s KeyStatus) IsUsed() bool { + return s == KeyUsed +} + +func (s KeyStatus) IsDiscard() bool { + return s == KeyDiscard +} + +func (s KeyStatus) IsExpire() bool { + return s == KeyExpire +} diff --git a/cmd/rsa/main.go b/cmd/rsa/main.go new file mode 100644 index 0000000..b1e748b --- /dev/null +++ b/cmd/rsa/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/utils/rsa" +) + +func main() { + n := rsa.NewGenerateKey() + err := n.SavePem("../../pem") + if err != nil { + panic(err) + } + privateKeyStr, publicKeyStr := n.GetKey() + fmt.Println("privateKeyStr=", privateKeyStr) + fmt.Println("publicKeyStr=", publicKeyStr) + + fmt.Println("aesKey=", rsa.GenerateAesKey()) +} diff --git a/cmd/sm/main.go b/cmd/sm/main.go new file mode 100644 index 0000000..8faf4e0 --- /dev/null +++ b/cmd/sm/main.go @@ -0,0 +1,20 @@ +package main + +import ( + "fmt" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/utils/sm" +) + +func main() { + priK, pubK, err := sm.GenerateKey() + if err != nil { + panic(err) + } + fmt.Println("privateKeyStr=", priK) + fmt.Println("publicKeyStr=", pubK) + sm4key, err := sm.GenerateSM4Key() + if err != nil { + panic(err) + } + fmt.Println("sm4key=", sm4key) +} diff --git a/core/config.go b/core/config.go new file mode 100644 index 0000000..9712dc1 --- /dev/null +++ b/core/config.go @@ -0,0 +1,90 @@ +package core + +import ( + "fmt" + "github.com/go-playground/validator/v10" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/utils/rsa" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/utils/sm" +) + +type SignType string + +const ( + SignRSA SignType = "RSA" + SignSM SignType = "SM" +) + +// Config merchant app Config +type Config struct { + AppID string `validate:"required"` + PrivateKey string `validate:"required"` + PublicKey string `validate:"required"` + Key string `validate:"required"` + SignType SignType `validate:"required"` + BaseURL string `validate:"required,url"` +} + +func (c *Config) Validate() error { + if err := validator.New().Struct(c); err != nil { + for _, err = range err.(validator.ValidationErrors) { + return err + } + } + return nil +} + +func (s SignType) IsRSA() bool { + return s == SignRSA +} + +func (s SignType) IsSM() bool { + return s == SignSM +} + +func (c *Config) CryptographySuite() (*CryptographySuite, error) { + if c.SignType.IsRSA() { + return c.CryptographySuiteRSA() + } + + if c.SignType.IsSM() { + return c.CryptographySuiteSM() + } + + return nil, fmt.Errorf("[%s] invalid sign type", c.SignType) +} + +func (c *Config) CryptographySuiteRSA() (*CryptographySuite, error) { + prk, err := rsa.PrivateKeyRSA(c.PrivateKey) + if err != nil { + return nil, err + } + + puk, err := rsa.PublicKeyRSA(c.PublicKey) + if err != nil { + return nil, err + } + + return &CryptographySuite{ + Signer: &RsaSigner{privateKey: prk}, + Verifier: &RsaVerifier{publicKey: puk}, + Cipher: &RsaEncodeDecode{key: c.Key}, + }, nil +} + +func (c *Config) CryptographySuiteSM() (*CryptographySuite, error) { + prk, err := sm.PrivateKeySM(c.PrivateKey) + if err != nil { + return nil, err + } + + puk, err := sm.PublicKeySM(c.PublicKey) + if err != nil { + return nil, err + } + + return &CryptographySuite{ + Signer: &SmSigner{privateKey: prk}, + Verifier: &SmVerifier{publicKey: puk}, + Cipher: &SmEncodeDecode{key: c.Key}, + }, nil +} diff --git a/core/core.go b/core/core.go new file mode 100644 index 0000000..09b94c7 --- /dev/null +++ b/core/core.go @@ -0,0 +1,249 @@ +package core + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/utils" + "io" + "net/http" + "time" +) + +// Params request params +type Params struct { + // AppId app id + AppId string `json:"app_id"` + // SignType sign type + SignType SignType `json:"sign_type"` + // Timestamp 发送请求的时间,格式"yyyy-MM-dd HH:mm:ss" + Timestamp string `json:"timestamp"` + // Sign + Sign string `json:"sign"` + // Ciphertext + Ciphertext string `json:"ciphertext"` +} + +// Core structure +type Core struct { + // HttpClient http client + HttpClient *http.Client + // Config config + Config *Config + // CryptographySuite + CryptographySuite *CryptographySuite +} + +type Option func(*Core) + +// WithHttpClient sets the http client +func WithHttpClient(client *http.Client) Option { + return func(s *Core) { + s.HttpClient = client + } +} + +// NewCore creates a new Core instance +func NewCore(c *Config, o ...Option) (*Core, error) { + + if err := c.Validate(); err != nil { + return nil, err + } + + core := &Core{ + HttpClient: http.DefaultClient, + Config: c, + } + for _, f := range o { + f(core) + } + + crs, err := c.CryptographySuite() + if err != nil { + return nil, err + } + + core.CryptographySuite = crs + + return core, nil +} + +// GetBizSignStr gets the biz sign str Go 版本是 1.15 或更高版本 +func (c *Core) GetBizSignStr(request any) (plaintext string, err error) { + + kvs := utils.SortStruct(request) + + kvm := make(map[string]any, len(kvs)) + var order []string + + for _, kv := range kvs { + kvm[kv.Key] = kv.Value + order = append(order, kv.Key) + } + + orderedMap := make(map[string]any) + for _, key := range order { + orderedMap[key] = kvm[key] + } + + // 将 orderedMap 转换成 JSON 字符串,保持顺序 + kvmBytes, err := json.Marshal(orderedMap) + if err != nil { + return "", err + } + + return string(kvmBytes), nil +} + +// GetCiphertext gets the ciphertext +func (c *Core) GetCiphertext(request any) (string, error) { + + plaintext, err := c.GetBizSignStr(request) + if err != nil { + return "", err + } + + ciphertext, err := c.CryptographySuite.Cipher.Encode(plaintext) + if err != nil { + return "", err + } + + return ciphertext, nil +} + +// BuildParams gets the params +func (c *Core) BuildParams(request Request) (*Params, error) { + + if err := request.Validate(); err != nil { + return nil, err + } + + ciphertext, err := c.GetCiphertext(request) + if err != nil { + return nil, err + } + + timestamps := time.Now().Format(time.DateTime) + dataToSign := c.Config.AppID + timestamps + ciphertext + + signature, err := c.CryptographySuite.Signer.Sign(dataToSign) + if err != nil { + return nil, err + } + + return &Params{ + AppId: c.Config.AppID, + SignType: c.Config.SignType, + Timestamp: timestamps, + Sign: signature, + Ciphertext: ciphertext, + }, nil +} + +// BuildAnyApiParams gets the params +func (c *Core) BuildAnyApiParams(bizContent any) (*Params, error) { + + ciphertext, err := c.GetCiphertext(bizContent) + if err != nil { + return nil, err + } + + timestamps := time.Now().Format(time.DateTime) + dataToSign := c.Config.AppID + timestamps + ciphertext + + signature, err := c.CryptographySuite.Signer.Sign(dataToSign) + if err != nil { + return nil, err + } + + return &Params{ + AppId: c.Config.AppID, + SignType: c.Config.SignType, + Timestamp: timestamps, + Sign: signature, + Ciphertext: ciphertext, + }, nil +} + +// Verify verifies the params +func (c *Core) Verify(timestamp, ciphertext, sign string) bool { + dataToSign := c.Config.AppID + timestamp + ciphertext + return c.CryptographySuite.Verifier.Verify(dataToSign, sign) +} + +func (c *Core) GetHeaders(p *Params) http.Header { + + h := http.Header{} + + h.Set("Content-Type", "application/json") + h.Set("Version", "1.0") + h.Set("Appid", c.Config.AppID) + h.Set("Sign-Type", string(c.Config.SignType)) + + h.Set("Timestamp", p.Timestamp) + h.Set("Sign", p.Sign) + + return h +} + +// GetRequestBody gets the request body +func (c *Core) GetRequestBody(_ context.Context, request Request) (http.Header, []byte, error) { + + p, err := c.BuildParams(request) + if err != nil { + return nil, nil, err + } + + h := c.GetHeaders(p) + + reqBodyBytes, err := json.Marshal(p) + if err != nil { + return nil, nil, err + } + + return h, reqBodyBytes, nil +} + +// Post sends the request and Analysis the response +func (c *Core) Post(ctx context.Context, method string, request Request) (http.Header, *http.Response, []byte, error) { + + h, reqBodyBytes, err := c.GetRequestBody(ctx, request) + if err != nil { + return nil, nil, nil, err + } + + httpResponse, body, err := c.Request(ctx, h, http.MethodPost, c.Config.BaseURL+method, reqBodyBytes) + if err != nil { + return nil, nil, nil, err + } + + return h, httpResponse, body, nil +} + +// Request sends the request and Analysis the response +func (c *Core) Request(ctx context.Context, h http.Header, method, url string, body []byte) (*http.Response, []byte, error) { + + req, err := http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(body)) + if err != nil { + return nil, nil, fmt.Errorf("failed to create HTTP request: %w", err) + } + req.Header = h + + resp, err := c.HttpClient.Do(req) + if err != nil { + return nil, nil, fmt.Errorf("sending HTTP request failed: %w", err) + } + defer resp.Body.Close() + + bodyBytes, err := io.ReadAll(resp.Body) + if err != nil { + return nil, nil, fmt.Errorf("failed to read response body: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, nil, fmt.Errorf("HTTP status code: %d", resp.StatusCode) + } + + return resp, bodyBytes, nil +} diff --git a/core/core_test.go b/core/core_test.go new file mode 100644 index 0000000..5242eb2 --- /dev/null +++ b/core/core_test.go @@ -0,0 +1,63 @@ +package core + +import ( + "net/http" + "testing" + "time" +) + +func TestRSASignVerify(t *testing.T) { + c := Config{ + AppID: "", + PrivateKey: "", + PublicKey: "", + Key: "", + SignType: SignRSA, + BaseURL: "http://127.0.0.1:9000", + } + + httpClient := &http.Client{ + Timeout: time.Second * 10, + } + core, err := NewCore(&c, WithHttpClient(httpClient)) + if err != nil { + t.Error(err) + return + } + signStr := "123456{}测试" + signature, err := core.CryptographySuite.Signer.Sign(signStr) + if err != nil { + t.Error(err) + return + } + b := core.CryptographySuite.Verifier.Verify(signStr, signature) + if !b { + t.Error("验签失败") + } +} + +func TestSMSignVerify(t *testing.T) { + c := Config{ + AppID: "123456", + PrivateKey: "zJRUcwPpKFf4nWiN9wqSO9gpGFx5BP4WviqnPsrhkpc=", + PublicKey: "BKbxGVVlJGWK/ScU0ebKSe4Jr4LvcBGgvt/HHBk+ODVCYnJYvvmX8cDNpf3TVYuRdz/RUH6UDgcoVpz02jXNfrM=", + Key: "t+VxHnp+K9huhtNT84Pk7A==", + SignType: SignSM, + BaseURL: "http://127.0.0.1:9000", + } + core, err := NewCore(&c) + if err != nil { + t.Error(err) + return + } + signStr := "123456{}测试" + signature, err := core.CryptographySuite.Signer.Sign(signStr) + if err != nil { + t.Error(err) + return + } + b := core.CryptographySuite.Verifier.Verify(signStr, signature) + if !b { + t.Error("验签失败") + } +} diff --git a/core/interface.go b/core/interface.go new file mode 100644 index 0000000..265a722 --- /dev/null +++ b/core/interface.go @@ -0,0 +1,30 @@ +package core + +// Signer interfaces for signing data +type Signer interface { + Sign(data string) (string, error) +} + +// Verifier interfaces for verifying signatures +type Verifier interface { + Verify(data, signature string) bool +} + +// Cipher interfaces for Encode or Decode request +type Cipher interface { + Encode(plaintext string) (string, error) + Decode(ciphertext string) (string, error) +} + +// CryptographySuite . +type CryptographySuite struct { + Signer Signer + Verifier Verifier + Cipher Cipher +} + +// Request interfaces for request +type Request interface { + String() (string, error) + Validate() error +} diff --git a/core/response.go b/core/response.go new file mode 100644 index 0000000..fe41662 --- /dev/null +++ b/core/response.go @@ -0,0 +1,28 @@ +package core + +import ( + "encoding/json" +) + +const SuccessCode = 200 + +type Response struct { + Code int32 `json:"code"` + Message string `json:"message"` + Reason string `json:"reason,omitempty"` + Data json.RawMessage `json:"data,omitempty"` +} + +func BuildResponse(b []byte) (*Response, error) { + + var resp Response + if err := json.Unmarshal(b, &resp); err != nil { + return nil, err + } + + return &resp, nil +} + +func (a *Response) IsSuccess() bool { + return a.Code == SuccessCode +} diff --git a/core/rsa.go b/core/rsa.go new file mode 100644 index 0000000..216f26d --- /dev/null +++ b/core/rsa.go @@ -0,0 +1,41 @@ +package core + +import ( + "crypto/rsa" + sdkrsa "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/utils/rsa" +) + +// RsaSigner for RSA signing +type RsaSigner struct { + privateKey *rsa.PrivateKey +} + +// RsaVerifier for RSA verification +type RsaVerifier struct { + publicKey *rsa.PublicKey +} + +// RsaEncodeDecode . +type RsaEncodeDecode struct { + key string +} + +func (r *RsaSigner) Sign(data string) (string, error) { + return sdkrsa.Sign(data, r.privateKey) +} + +func (r *RsaVerifier) Verify(data, signature string) bool { + b, err := sdkrsa.Verify(data, signature, r.publicKey) + if err != nil { + return false + } + return b +} + +func (r *RsaEncodeDecode) Encode(plaintext string) (string, error) { + return sdkrsa.Encode(r.key, plaintext), nil +} + +func (r *RsaEncodeDecode) Decode(data string) (string, error) { + return sdkrsa.Decode(r.key, data), nil +} diff --git a/core/sm.go b/core/sm.go new file mode 100644 index 0000000..a8563d2 --- /dev/null +++ b/core/sm.go @@ -0,0 +1,41 @@ +package core + +import ( + "github.com/sleepinggodoflove/lansexiongdi-marketing-sdk/utils/sm" + "github.com/tjfoc/gmsm/sm2" +) + +// SmSigner for SM signing (国密) +type SmSigner struct { + privateKey *sm2.PrivateKey +} + +// SmVerifier for SM verification (国密) +type SmVerifier struct { + publicKey *sm2.PublicKey +} + +// SmEncodeDecode . +type SmEncodeDecode struct { + key string +} + +func (s *SmSigner) Sign(data string) (string, error) { + return sm.Sign(data, s.privateKey) +} + +func (s *SmVerifier) Verify(data, signature string) bool { + b, err := sm.Verify(data, signature, s.publicKey) + if err != nil { + return false + } + return b +} + +func (s *SmEncodeDecode) Encode(plaintext string) (string, error) { + return sm.Encode(s.key, []byte(plaintext)) +} + +func (s *SmEncodeDecode) Decode(ciphertext string) (string, error) { + return sm.Decode(s.key, ciphertext) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..96d4626 --- /dev/null +++ b/go.mod @@ -0,0 +1,23 @@ +module github.com/sleepinggodoflove/lansexiongdi-marketing-sdk + +go 1.22.2 + +require ( + github.com/go-playground/validator/v10 v10.22.1 + github.com/stretchr/testify v1.9.0 + github.com/tjfoc/gmsm v1.4.1 +) + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + golang.org/x/crypto v0.19.0 // indirect + golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.17.0 // indirect + golang.org/x/text v0.14.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..75f43c6 --- /dev/null +++ b/go.sum @@ -0,0 +1,101 @@ +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= +github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA= +github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= +github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho= +github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= +golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= +golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= +golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= +golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= +golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= +google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= +google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= +google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/utils/rsa/aes.go b/utils/rsa/aes.go new file mode 100644 index 0000000..5671839 --- /dev/null +++ b/utils/rsa/aes.go @@ -0,0 +1,122 @@ +package rsa + +import ( + "crypto/aes" + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "encoding/hex" +) + +func GenerateAesKey() string { + key := make([]byte, 16) + if _, err := rand.Read(key); err != nil { + panic(err) + } + return hex.EncodeToString(key) +} + +// Encode 加密函数 +func Encode(key, plaintext string) string { + // 创建AES加密器 + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return "" + } + + // PKCS7填充 + blockSize := block.BlockSize() + padding := blockSize - len(plaintext)%blockSize + padText := string(byte(padding)) + for i := 0; i < padding; i++ { + plaintext += padText + } + + // 创建ECB模式加密器 + mode := NewECEncrypted(block) + + // 计算加密后数据的长度 + encrypted := make([]byte, len(plaintext)) + mode.CryptBlocks(encrypted, []byte(plaintext)) + + // Base64编码 + return base64.StdEncoding.EncodeToString(encrypted) +} + +// Decode 解密函数 +func Decode(key, code string) string { + // Base64解码 + encryptString, err := base64.StdEncoding.DecodeString(code) + if err != nil { + return "" + } + + // 创建AES解密器 + block, err := aes.NewCipher([]byte(key)) + if err != nil { + return "" + } + + // 创建ECB模式解密器 + mode := NewECDecrypted(block) + + // 解密 + decrypted := make([]byte, len(encryptString)) + mode.CryptBlocks(decrypted, encryptString) + + // 去除填充 + padding := decrypted[len(decrypted)-1] + return string(decrypted[:len(decrypted)-int(padding)]) +} + +// ecEncrypted ECB加密器 +type ecEncrypted struct { + b cipher.Block + blockSize int +} + +func NewECEncrypted(b cipher.Block) cipher.BlockMode { + return &ecEncrypted{b, b.BlockSize()} +} + +func (x *ecEncrypted) BlockSize() int { return x.blockSize } + +func (x *ecEncrypted) CryptBlocks(dst, src []byte) { + if len(src)%x.blockSize != 0 { + panic("src not full blocks") + } + if len(dst) < len(src) { + panic("output smaller than input") + } + for len(src) > 0 { + x.b.Encrypt(dst, src[:x.blockSize]) + src = src[x.blockSize:] + dst = dst[x.blockSize:] + } +} + +// ECB解密器 +type ecDecrypted struct { + b cipher.Block + blockSize int +} + +func NewECDecrypted(b cipher.Block) cipher.BlockMode { + return &ecDecrypted{b, b.BlockSize()} +} + +func (x *ecDecrypted) BlockSize() int { return x.blockSize } + +func (x *ecDecrypted) CryptBlocks(dst, src []byte) { + if len(src)%x.blockSize != 0 { + panic("src not full blocks") + } + if len(dst) < len(src) { + panic("output smaller than input") + } + for len(src) > 0 { + x.b.Decrypt(dst, src[:x.blockSize]) + src = src[x.blockSize:] + dst = dst[x.blockSize:] + } +} diff --git a/utils/rsa/aes_test.go b/utils/rsa/aes_test.go new file mode 100644 index 0000000..9ee4b27 --- /dev/null +++ b/utils/rsa/aes_test.go @@ -0,0 +1,31 @@ +package rsa + +import "testing" + +func TestGenerateAesKey(t *testing.T) { + s := GenerateAesKey() + t.Log(s) +} + +func TestEncode(t *testing.T) { + key := "870abfc720f86ce2c5e4d3345741d48d" + str := "123yie一二三" + e := Encode(key, str) + t.Log(e) +} + +func TestDecode(t *testing.T) { + key := "870abfc720f86ce2c5e4d3345741d48d" + e := "xxxx" + d := Decode(key, e) + t.Log(d) +} + +func TestEncodeDecode(t *testing.T) { + key := "870abfc720f86ce2c5e4d3345741d48d" + str := "123yie一二三" + e := Encode(key, str) + t.Log(e) + d := Decode(key, e) + t.Log(d) +} diff --git a/utils/rsa/cipher.go b/utils/rsa/cipher.go new file mode 100644 index 0000000..41f80a3 --- /dev/null +++ b/utils/rsa/cipher.go @@ -0,0 +1,30 @@ +package rsa + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" + "fmt" +) + +func Cipher(publicKey *rsa.PublicKey, plaintext []byte) (string, error) { + ciphertext, err := rsa.EncryptOAEP(sha256.New(), rand.Reader, publicKey, plaintext, nil) + if err != nil { + return "", fmt.Errorf("error encrypting data: %v", err) + } + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +func Plain(privateKey *rsa.PrivateKey, ciphertext string) (string, error) { + ciphertextBytes, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", fmt.Errorf("error decoding base64: %v", err) + } + decryptedData, err := privateKey.Decrypt(nil, ciphertextBytes, &rsa.OAEPOptions{Hash: crypto.SHA256}) + if err != nil { + return "", fmt.Errorf("error decrypting data: %v", err) + } + return string(decryptedData), nil +} diff --git a/utils/rsa/cipher_test.go b/utils/rsa/cipher_test.go new file mode 100644 index 0000000..790bb10 --- /dev/null +++ b/utils/rsa/cipher_test.go @@ -0,0 +1,43 @@ +package rsa + +import ( + "testing" +) + +func TestCipher(t *testing.T) { + publicKeyStr := "MIIBCgKCAQEA10X/4v44TU0sPOKhnHLx7Qe0SREVIne9+3DEtTCbzrewXSrlHedMT6RVlwv1i0D6q5zWGV9WXMI4ZrM2EM6wE4bZfjZU5FvzYTQN8fjiBQTggU8BDFpUPGibwt2Fyv6SAnj0AQzD7itvcxydu0tDOgg9Aaa701kxhz9yZukpZGVhAEBussRc41EuEwE2gQJ5prSFijzhqg5awwAJESX9gDHX3DncLO2s3FuaaReJ1U/c/LsJIvRGPckCllSJM1s9WAvjz5yFXZMpvWpmAnmpxd5fNd349Gr/swfM50TN5D3cTJblO771FifNFm6r+4o1nkR/thWX5vMasvKwimGo3QIDAQAB" + pukRsa, err := PublicKeyRSA(publicKeyStr) + if err != nil { + t.Fatal(err) + } + plaintext := []byte("123一二三{\"key\":\"value\"}") + cipherStr, err := Cipher(pukRsa, plaintext) + if err != nil { + t.Fatal(err) + } + t.Log(cipherStr) +} + +func TestPlain(t *testing.T) { + publicKeyStr := "MIIBCgKCAQEA10X/4v44TU0sPOKhnHLx7Qe0SREVIne9+3DEtTCbzrewXSrlHedMT6RVlwv1i0D6q5zWGV9WXMI4ZrM2EM6wE4bZfjZU5FvzYTQN8fjiBQTggU8BDFpUPGibwt2Fyv6SAnj0AQzD7itvcxydu0tDOgg9Aaa701kxhz9yZukpZGVhAEBussRc41EuEwE2gQJ5prSFijzhqg5awwAJESX9gDHX3DncLO2s3FuaaReJ1U/c/LsJIvRGPckCllSJM1s9WAvjz5yFXZMpvWpmAnmpxd5fNd349Gr/swfM50TN5D3cTJblO771FifNFm6r+4o1nkR/thWX5vMasvKwimGo3QIDAQAB" + pukRsa, err := PublicKeyRSA(publicKeyStr) + if err != nil { + t.Fatal(err) + } + plaintext := []byte("123一二三{\"key\":\"value\"}") + cipherStr, err := Cipher(pukRsa, plaintext) + if err != nil { + t.Fatal(err) + } + t.Log(cipherStr) + rsaPrivateKey := "MIIEpQIBAAKCAQEA10X/4v44TU0sPOKhnHLx7Qe0SREVIne9+3DEtTCbzrewXSrlHedMT6RVlwv1i0D6q5zWGV9WXMI4ZrM2EM6wE4bZfjZU5FvzYTQN8fjiBQTggU8BDFpUPGibwt2Fyv6SAnj0AQzD7itvcxydu0tDOgg9Aaa701kxhz9yZukpZGVhAEBussRc41EuEwE2gQJ5prSFijzhqg5awwAJESX9gDHX3DncLO2s3FuaaReJ1U/c/LsJIvRGPckCllSJM1s9WAvjz5yFXZMpvWpmAnmpxd5fNd349Gr/swfM50TN5D3cTJblO771FifNFm6r+4o1nkR/thWX5vMasvKwimGo3QIDAQABAoIBAQCQkaXi3y8YWrdWvCwkUN0/fWkJmLtExn2Dmpu/wsEf9iQurVvo1SheY9JG+fUQa7bsAQuXRntNF/GgpsGsT+HXezwckog4Q7gSk066LZY8IKZUsKXXkeH4H5hbKUFsrcGIf4n+GoCKNglGmPUkjsq68kVmEn8Y1FF6rpU5n2P40xEKAieKxlM2JNwR22DQYRw3iw4PcMAD88nKx9OBUwGig8MQnUka7OCZk9fNLdwBT0VfgCzRdvyBCDieif4vB7TnMmvYlr6wWOMi2Ad9ccY2wTlVOUyHoC6BZ72FgOYyfnAmEZbDChCNTEDNNj0m056slCTMO+nIVUqMip4imgiBAoGBAOWI84+4dRA+gm6xynXsp9TAto43/DmbohHrUWRE2tSGqPevBz2i0c5AOaNUdxtzoEWOj260Zdzf3gRhv/iH3Mgp2+R2+cJ1QYoaX/2auJ50dPJf1SHZrKVIYhYqqlIWc8jQo8XBK/Ys/Lf+N2We2EsMLMtUUMH9OZ+20XtlxQBZAoGBAPAYGVOzJnqFuSqxFzAA7VXP6p9WKxGEzbDUCxFaLj751DnvogI8FczAE6ADBxNYVkeQYtvqzMb9nTollOL6+/T9MUJn3DXTT8/St+REVdWvmO9Az9nGVDkLGjz0CH0iMjN49iSMFsdmo6L2528kUOj20dPh5IkzyWykYqni+fwlAoGBAMa1A51U40rXwpTPp2TlJfnBh4ihIOJCQFDg9YonLYY0uUwKousR7C1wXjVuJtqGA6aTnsoIs/I9f3ctpEIkY9aInksvUFKurbk/0f+7FL5gNOmqWtk+Fv7TJc7oyp/bvgqHzG+jJkqscW9bTVvU4ow9kv3HFU6KyHriioEX/i6pAoGBAJs5yW4e3lrKh/u9ANPNVaRsRzF64V9zMBUKEpnGZy3aEcbfUiwFssZszINgUbvFGgsso22xcXGZ2IQWdhsFz84FwEpBodK+6tPfVXrkX2ZHICZXDcqrehpjPjR4ReC5MiGrK+BXHgcPKe6bmOd3YEQuB1zop/u4mpp98TgLAjptAoGAF1lgcmwnHDduvWErwSmMR3w45FppV0pOdSEmv/6DU6iglzleSuuCm4fhXAEgSahJMKiflNx1ufl84IGir0r/we8GNfs4ceSPlyKDYnaBG19f5jWLLNFTWfwVhZqa85pd9b2dcqVWMazHq8psmyt0opmctRcM9s4lNq2OmvB7vM4=" + prkRsa, err := PrivateKeyRSA(rsaPrivateKey) + if err != nil { + t.Fatal(err) + } + plainStr, err := Plain(prkRsa, cipherStr) + if err != nil { + t.Fatal(err) + } + t.Log(plainStr) +} diff --git a/utils/rsa/generate.go b/utils/rsa/generate.go new file mode 100644 index 0000000..ed8bd8d --- /dev/null +++ b/utils/rsa/generate.go @@ -0,0 +1,59 @@ +package rsa + +import ( + "crypto/rand" + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "log" + "os" +) + +type Generate struct { + publicKeyBytes []byte + privateKeyBytes []byte +} + +func NewGenerateKey() *Generate { + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + log.Fatal(err) + } + return &Generate{ + publicKeyBytes: x509.MarshalPKCS1PublicKey(&privateKey.PublicKey), + privateKeyBytes: x509.MarshalPKCS1PrivateKey(privateKey), + } +} + +func (g *Generate) SavePem(path string) error { + if _, err := os.Stat(path); os.IsNotExist(err) { + if err = os.MkdirAll(path, os.ModePerm); err != nil { + return err + } + } + + privateKeyFile, err := os.Create(path + "/private.pem") + if err != nil { + return err + } + defer privateKeyFile.Close() + + if err = pem.Encode(privateKeyFile, &pem.Block{Type: "RSA PRIVATE KEY", Bytes: g.privateKeyBytes}); err != nil { + return err + } + + publicKeyFile, err := os.Create(path + "/public.pem") + if err != nil { + return err + } + defer publicKeyFile.Close() + + return pem.Encode(publicKeyFile, &pem.Block{Type: "RSA PUBLIC KEY", Bytes: g.publicKeyBytes}) +} + +func (g *Generate) GetKey() (privateKeyStr, publicKeyStr string) { + privateKeyStr = base64.StdEncoding.EncodeToString(g.privateKeyBytes) + publicKeyStr = base64.StdEncoding.EncodeToString(g.publicKeyBytes) + return +} diff --git a/utils/rsa/generate_test.go b/utils/rsa/generate_test.go new file mode 100644 index 0000000..83938c2 --- /dev/null +++ b/utils/rsa/generate_test.go @@ -0,0 +1,29 @@ +package rsa + +import "testing" + +func TestGenerate(t *testing.T) { + n := NewGenerateKey() + err := n.SavePem("../../pem") + if err != nil { + t.Fatal(err) + } + privateKeyStr, publicKeyStr := n.GetKey() + t.Log("privateKeyStr=", privateKeyStr) + t.Log("publicKeyStr=", publicKeyStr) +} + +func TestGenerateSavePem(t *testing.T) { + n := NewGenerateKey() + err := n.SavePem("../../pem") + if err != nil { + t.Fatal(err) + } +} + +func TestGenerateGetKey(t *testing.T) { + n := NewGenerateKey() + privateKeyStr, publicKeyStr := n.GetKey() + t.Log("privateKeyStr=", privateKeyStr) + t.Log("publicKeyStr=", publicKeyStr) +} diff --git a/utils/rsa/pem.go b/utils/rsa/pem.go new file mode 100644 index 0000000..549d8a9 --- /dev/null +++ b/utils/rsa/pem.go @@ -0,0 +1,75 @@ +package rsa + +import ( + "crypto/rsa" + "crypto/x509" + "encoding/base64" + "encoding/pem" + "fmt" + "log" +) + +func PrivateKeyRSA(privateKeyStr string) (*rsa.PrivateKey, error) { + privateKeyBytes, err := base64.StdEncoding.DecodeString(privateKeyStr) + if err != nil { + return nil, fmt.Errorf("解码 Base64 编码的 RSA 私钥字符串: %v", err) + } + + privateKey, err := x509.ParsePKCS1PrivateKey(privateKeyBytes) + if err != nil { + return nil, fmt.Errorf("解析 RSA 私钥: %v", err) + } + return privateKey, nil +} + +func PrivateKeyPem(privateKeyStr string) (string, error) { + privateKeyBytes, err := base64.StdEncoding.DecodeString(privateKeyStr) + if err != nil { + return "", fmt.Errorf("解码 Base64 编码的 RSA 私钥字符串: %v", err) + } + + privateKey, err := x509.ParsePKCS1PrivateKey(privateKeyBytes) + if err != nil { + return "", fmt.Errorf("解析 RSA 私钥: %v", err) + } + + privateKeyPEM := &pem.Block{ + Type: "RSA PRIVATE KEY", + Bytes: x509.MarshalPKCS1PrivateKey(privateKey), + } + + return string(pem.EncodeToMemory(privateKeyPEM)), nil +} + +func PublicKeyRSA(publicKeyStr string) (*rsa.PublicKey, error) { + publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyStr) + if err != nil { + return nil, fmt.Errorf("解码 Base64 编码的 RSA 公钥字符串: %v", err) + } + + publicKey, err := x509.ParsePKCS1PublicKey(publicKeyBytes) + if err != nil { + return nil, fmt.Errorf("解析 RSA 公钥: %v", err) + } + + return publicKey, nil +} + +func PublicKeyPem(publicKeyStr string) (string, error) { + publicKeyBytes, err := base64.StdEncoding.DecodeString(publicKeyStr) + if err != nil { + return "", fmt.Errorf("解码 Base64 编码的 RSA 公钥字符串: %v", err) + } + + publicKey, err := x509.ParsePKCS1PublicKey(publicKeyBytes) + if err != nil { + log.Fatal("解析 RSA 公钥:", err) + } + + publicKeyPEM := &pem.Block{ + Type: "RSA PUBLIC KEY", + Bytes: x509.MarshalPKCS1PublicKey(publicKey), + } + + return string(pem.EncodeToMemory(publicKeyPEM)), nil +} diff --git a/utils/rsa/pem_test.go b/utils/rsa/pem_test.go new file mode 100644 index 0000000..1e8eeb6 --- /dev/null +++ b/utils/rsa/pem_test.go @@ -0,0 +1,21 @@ +package rsa + +import "testing" + +func TestPrivateKeyPem(t *testing.T) { + privateKeyStr := "MIIEpQIBAAKCAQEA10X/4v44TU0sPOKhnHLx7Qe0SREVIne9+3DEtTCbzrewXSrlHedMT6RVlwv1i0D6q5zWGV9WXMI4ZrM2EM6wE4bZfjZU5FvzYTQN8fjiBQTggU8BDFpUPGibwt2Fyv6SAnj0AQzD7itvcxydu0tDOgg9Aaa701kxhz9yZukpZGVhAEBussRc41EuEwE2gQJ5prSFijzhqg5awwAJESX9gDHX3DncLO2s3FuaaReJ1U/c/LsJIvRGPckCllSJM1s9WAvjz5yFXZMpvWpmAnmpxd5fNd349Gr/swfM50TN5D3cTJblO771FifNFm6r+4o1nkR/thWX5vMasvKwimGo3QIDAQABAoIBAQCQkaXi3y8YWrdWvCwkUN0/fWkJmLtExn2Dmpu/wsEf9iQurVvo1SheY9JG+fUQa7bsAQuXRntNF/GgpsGsT+HXezwckog4Q7gSk066LZY8IKZUsKXXkeH4H5hbKUFsrcGIf4n+GoCKNglGmPUkjsq68kVmEn8Y1FF6rpU5n2P40xEKAieKxlM2JNwR22DQYRw3iw4PcMAD88nKx9OBUwGig8MQnUka7OCZk9fNLdwBT0VfgCzRdvyBCDieif4vB7TnMmvYlr6wWOMi2Ad9ccY2wTlVOUyHoC6BZ72FgOYyfnAmEZbDChCNTEDNNj0m056slCTMO+nIVUqMip4imgiBAoGBAOWI84+4dRA+gm6xynXsp9TAto43/DmbohHrUWRE2tSGqPevBz2i0c5AOaNUdxtzoEWOj260Zdzf3gRhv/iH3Mgp2+R2+cJ1QYoaX/2auJ50dPJf1SHZrKVIYhYqqlIWc8jQo8XBK/Ys/Lf+N2We2EsMLMtUUMH9OZ+20XtlxQBZAoGBAPAYGVOzJnqFuSqxFzAA7VXP6p9WKxGEzbDUCxFaLj751DnvogI8FczAE6ADBxNYVkeQYtvqzMb9nTollOL6+/T9MUJn3DXTT8/St+REVdWvmO9Az9nGVDkLGjz0CH0iMjN49iSMFsdmo6L2528kUOj20dPh5IkzyWykYqni+fwlAoGBAMa1A51U40rXwpTPp2TlJfnBh4ihIOJCQFDg9YonLYY0uUwKousR7C1wXjVuJtqGA6aTnsoIs/I9f3ctpEIkY9aInksvUFKurbk/0f+7FL5gNOmqWtk+Fv7TJc7oyp/bvgqHzG+jJkqscW9bTVvU4ow9kv3HFU6KyHriioEX/i6pAoGBAJs5yW4e3lrKh/u9ANPNVaRsRzF64V9zMBUKEpnGZy3aEcbfUiwFssZszINgUbvFGgsso22xcXGZ2IQWdhsFz84FwEpBodK+6tPfVXrkX2ZHICZXDcqrehpjPjR4ReC5MiGrK+BXHgcPKe6bmOd3YEQuB1zop/u4mpp98TgLAjptAoGAF1lgcmwnHDduvWErwSmMR3w45FppV0pOdSEmv/6DU6iglzleSuuCm4fhXAEgSahJMKiflNx1ufl84IGir0r/we8GNfs4ceSPlyKDYnaBG19f5jWLLNFTWfwVhZqa85pd9b2dcqVWMazHq8psmyt0opmctRcM9s4lNq2OmvB7vM4=" + prkPem, err := PrivateKeyPem(privateKeyStr) + if err != nil { + t.Fatal(err) + } + t.Log(prkPem) +} + +func TestPublicKeyPem(t *testing.T) { + publicKeyStr := "MIIBCgKCAQEA10X/4v44TU0sPOKhnHLx7Qe0SREVIne9+3DEtTCbzrewXSrlHedMT6RVlwv1i0D6q5zWGV9WXMI4ZrM2EM6wE4bZfjZU5FvzYTQN8fjiBQTggU8BDFpUPGibwt2Fyv6SAnj0AQzD7itvcxydu0tDOgg9Aaa701kxhz9yZukpZGVhAEBussRc41EuEwE2gQJ5prSFijzhqg5awwAJESX9gDHX3DncLO2s3FuaaReJ1U/c/LsJIvRGPckCllSJM1s9WAvjz5yFXZMpvWpmAnmpxd5fNd349Gr/swfM50TN5D3cTJblO771FifNFm6r+4o1nkR/thWX5vMasvKwimGo3QIDAQAB" + pukPem, err := PublicKeyPem(publicKeyStr) + if err != nil { + t.Fatal(err) + } + t.Log(pukPem) +} diff --git a/utils/rsa/sign.go b/utils/rsa/sign.go new file mode 100644 index 0000000..f0a46de --- /dev/null +++ b/utils/rsa/sign.go @@ -0,0 +1,31 @@ +package rsa + +import ( + "crypto" + "crypto/rand" + "crypto/rsa" + "crypto/sha256" + "encoding/base64" +) + +func Sign(data string, privateKey *rsa.PrivateKey) (string, error) { + hashed := sha256.Sum256([]byte(data)) + signature, err := rsa.SignPKCS1v15(rand.Reader, privateKey, crypto.SHA256, hashed[:]) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(signature), nil +} + +func Verify(data string, signature string, publicKey *rsa.PublicKey) (bool, error) { + signatureBytes, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return false, err + } + hashed := sha256.Sum256([]byte(data)) + err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hashed[:], signatureBytes) + if err != nil { + return false, nil + } + return true, nil +} diff --git a/utils/rsa/sign_test.go b/utils/rsa/sign_test.go new file mode 100644 index 0000000..c2dd32b --- /dev/null +++ b/utils/rsa/sign_test.go @@ -0,0 +1,63 @@ +package rsa + +import ( + "testing" +) + +func Test_Sign(t *testing.T) { + rsaPrivateKey := "MIIEpQIBAAKCAQEA10X/4v44TU0sPOKhnHLx7Qe0SREVIne9+3DEtTCbzrewXSrlHedMT6RVlwv1i0D6q5zWGV9WXMI4ZrM2EM6wE4bZfjZU5FvzYTQN8fjiBQTggU8BDFpUPGibwt2Fyv6SAnj0AQzD7itvcxydu0tDOgg9Aaa701kxhz9yZukpZGVhAEBussRc41EuEwE2gQJ5prSFijzhqg5awwAJESX9gDHX3DncLO2s3FuaaReJ1U/c/LsJIvRGPckCllSJM1s9WAvjz5yFXZMpvWpmAnmpxd5fNd349Gr/swfM50TN5D3cTJblO771FifNFm6r+4o1nkR/thWX5vMasvKwimGo3QIDAQABAoIBAQCQkaXi3y8YWrdWvCwkUN0/fWkJmLtExn2Dmpu/wsEf9iQurVvo1SheY9JG+fUQa7bsAQuXRntNF/GgpsGsT+HXezwckog4Q7gSk066LZY8IKZUsKXXkeH4H5hbKUFsrcGIf4n+GoCKNglGmPUkjsq68kVmEn8Y1FF6rpU5n2P40xEKAieKxlM2JNwR22DQYRw3iw4PcMAD88nKx9OBUwGig8MQnUka7OCZk9fNLdwBT0VfgCzRdvyBCDieif4vB7TnMmvYlr6wWOMi2Ad9ccY2wTlVOUyHoC6BZ72FgOYyfnAmEZbDChCNTEDNNj0m056slCTMO+nIVUqMip4imgiBAoGBAOWI84+4dRA+gm6xynXsp9TAto43/DmbohHrUWRE2tSGqPevBz2i0c5AOaNUdxtzoEWOj260Zdzf3gRhv/iH3Mgp2+R2+cJ1QYoaX/2auJ50dPJf1SHZrKVIYhYqqlIWc8jQo8XBK/Ys/Lf+N2We2EsMLMtUUMH9OZ+20XtlxQBZAoGBAPAYGVOzJnqFuSqxFzAA7VXP6p9WKxGEzbDUCxFaLj751DnvogI8FczAE6ADBxNYVkeQYtvqzMb9nTollOL6+/T9MUJn3DXTT8/St+REVdWvmO9Az9nGVDkLGjz0CH0iMjN49iSMFsdmo6L2528kUOj20dPh5IkzyWykYqni+fwlAoGBAMa1A51U40rXwpTPp2TlJfnBh4ihIOJCQFDg9YonLYY0uUwKousR7C1wXjVuJtqGA6aTnsoIs/I9f3ctpEIkY9aInksvUFKurbk/0f+7FL5gNOmqWtk+Fv7TJc7oyp/bvgqHzG+jJkqscW9bTVvU4ow9kv3HFU6KyHriioEX/i6pAoGBAJs5yW4e3lrKh/u9ANPNVaRsRzF64V9zMBUKEpnGZy3aEcbfUiwFssZszINgUbvFGgsso22xcXGZ2IQWdhsFz84FwEpBodK+6tPfVXrkX2ZHICZXDcqrehpjPjR4ReC5MiGrK+BXHgcPKe6bmOd3YEQuB1zop/u4mpp98TgLAjptAoGAF1lgcmwnHDduvWErwSmMR3w45FppV0pOdSEmv/6DU6iglzleSuuCm4fhXAEgSahJMKiflNx1ufl84IGir0r/we8GNfs4ceSPlyKDYnaBG19f5jWLLNFTWfwVhZqa85pd9b2dcqVWMazHq8psmyt0opmctRcM9s4lNq2OmvB7vM4=" + signStr := "123一二三{\"key\":\"value\"}" + prkRsa, err := PrivateKeyRSA(rsaPrivateKey) + if err != nil { + t.Fatal(err) + } + got, err := Sign(signStr, prkRsa) + if err != nil { + t.Fatalf("Sign() error = %v", err) + } + t.Log(got) +} + +func Test_Verify(t *testing.T) { + signStr := "123一二三{\"key\":\"value\"}" + signature := "0vQLV1LKzcExH4Nc7U37OwOoq+3o2Sdz8dfV9mME99L9YiaM5LN9dv9CaxQI4NSTxDTbJ9iTrScHE5TyquKRcyeJCW1qBEYySoYhiAdF9TxhVcyXYgbz4oMfjTF0J0C78hKZUb+mZeoeq7hgUsOhMQwmbumjoxKM6Y/rsZHfJQSwyty7Z4jc6BkHr8IZzPUvDlQkmwcnk4EWwx0au47fKVGxdW8dD2Gf0vstYiDN0MSy7BZnWh1/RY0g3EnjmmO7NvSJFxdlOqLwA9HR2ch2Fot/dxot2nSPK2+pj8k1+vZ1+ga/Ee2hmrRG5gSVmesBiI79NVzgPsrGLAscupz2Mg==" + publicKeyStr := "MIIBCgKCAQEA10X/4v44TU0sPOKhnHLx7Qe0SREVIne9+3DEtTCbzrewXSrlHedMT6RVlwv1i0D6q5zWGV9WXMI4ZrM2EM6wE4bZfjZU5FvzYTQN8fjiBQTggU8BDFpUPGibwt2Fyv6SAnj0AQzD7itvcxydu0tDOgg9Aaa701kxhz9yZukpZGVhAEBussRc41EuEwE2gQJ5prSFijzhqg5awwAJESX9gDHX3DncLO2s3FuaaReJ1U/c/LsJIvRGPckCllSJM1s9WAvjz5yFXZMpvWpmAnmpxd5fNd349Gr/swfM50TN5D3cTJblO771FifNFm6r+4o1nkR/thWX5vMasvKwimGo3QIDAQAB" + pukRsa, err := PublicKeyRSA(publicKeyStr) + if err != nil { + t.Fatal(err) + } + got, err := Verify(signStr, signature, pukRsa) + if err != nil { + t.Fatal(err) + } + if !got { + t.Fatal("Verify() error = false") + } +} + +func Test_SignVerify(t *testing.T) { + rsaPrivateKey := "MIIEpQIBAAKCAQEA10X/4v44TU0sPOKhnHLx7Qe0SREVIne9+3DEtTCbzrewXSrlHedMT6RVlwv1i0D6q5zWGV9WXMI4ZrM2EM6wE4bZfjZU5FvzYTQN8fjiBQTggU8BDFpUPGibwt2Fyv6SAnj0AQzD7itvcxydu0tDOgg9Aaa701kxhz9yZukpZGVhAEBussRc41EuEwE2gQJ5prSFijzhqg5awwAJESX9gDHX3DncLO2s3FuaaReJ1U/c/LsJIvRGPckCllSJM1s9WAvjz5yFXZMpvWpmAnmpxd5fNd349Gr/swfM50TN5D3cTJblO771FifNFm6r+4o1nkR/thWX5vMasvKwimGo3QIDAQABAoIBAQCQkaXi3y8YWrdWvCwkUN0/fWkJmLtExn2Dmpu/wsEf9iQurVvo1SheY9JG+fUQa7bsAQuXRntNF/GgpsGsT+HXezwckog4Q7gSk066LZY8IKZUsKXXkeH4H5hbKUFsrcGIf4n+GoCKNglGmPUkjsq68kVmEn8Y1FF6rpU5n2P40xEKAieKxlM2JNwR22DQYRw3iw4PcMAD88nKx9OBUwGig8MQnUka7OCZk9fNLdwBT0VfgCzRdvyBCDieif4vB7TnMmvYlr6wWOMi2Ad9ccY2wTlVOUyHoC6BZ72FgOYyfnAmEZbDChCNTEDNNj0m056slCTMO+nIVUqMip4imgiBAoGBAOWI84+4dRA+gm6xynXsp9TAto43/DmbohHrUWRE2tSGqPevBz2i0c5AOaNUdxtzoEWOj260Zdzf3gRhv/iH3Mgp2+R2+cJ1QYoaX/2auJ50dPJf1SHZrKVIYhYqqlIWc8jQo8XBK/Ys/Lf+N2We2EsMLMtUUMH9OZ+20XtlxQBZAoGBAPAYGVOzJnqFuSqxFzAA7VXP6p9WKxGEzbDUCxFaLj751DnvogI8FczAE6ADBxNYVkeQYtvqzMb9nTollOL6+/T9MUJn3DXTT8/St+REVdWvmO9Az9nGVDkLGjz0CH0iMjN49iSMFsdmo6L2528kUOj20dPh5IkzyWykYqni+fwlAoGBAMa1A51U40rXwpTPp2TlJfnBh4ihIOJCQFDg9YonLYY0uUwKousR7C1wXjVuJtqGA6aTnsoIs/I9f3ctpEIkY9aInksvUFKurbk/0f+7FL5gNOmqWtk+Fv7TJc7oyp/bvgqHzG+jJkqscW9bTVvU4ow9kv3HFU6KyHriioEX/i6pAoGBAJs5yW4e3lrKh/u9ANPNVaRsRzF64V9zMBUKEpnGZy3aEcbfUiwFssZszINgUbvFGgsso22xcXGZ2IQWdhsFz84FwEpBodK+6tPfVXrkX2ZHICZXDcqrehpjPjR4ReC5MiGrK+BXHgcPKe6bmOd3YEQuB1zop/u4mpp98TgLAjptAoGAF1lgcmwnHDduvWErwSmMR3w45FppV0pOdSEmv/6DU6iglzleSuuCm4fhXAEgSahJMKiflNx1ufl84IGir0r/we8GNfs4ceSPlyKDYnaBG19f5jWLLNFTWfwVhZqa85pd9b2dcqVWMazHq8psmyt0opmctRcM9s4lNq2OmvB7vM4=" + signStr := "123一二三{\"key\":\"value\"}" + prkPem, err := PrivateKeyRSA(rsaPrivateKey) + if err != nil { + t.Fatal(err) + } + signature, err := Sign(signStr, prkPem) + if err != nil { + t.Fatalf("Sign() error = %v", err) + } + t.Log(signature) + + publicKeyStr := "MIIBCgKCAQEA10X/4v44TU0sPOKhnHLx7Qe0SREVIne9+3DEtTCbzrewXSrlHedMT6RVlwv1i0D6q5zWGV9WXMI4ZrM2EM6wE4bZfjZU5FvzYTQN8fjiBQTggU8BDFpUPGibwt2Fyv6SAnj0AQzD7itvcxydu0tDOgg9Aaa701kxhz9yZukpZGVhAEBussRc41EuEwE2gQJ5prSFijzhqg5awwAJESX9gDHX3DncLO2s3FuaaReJ1U/c/LsJIvRGPckCllSJM1s9WAvjz5yFXZMpvWpmAnmpxd5fNd349Gr/swfM50TN5D3cTJblO771FifNFm6r+4o1nkR/thWX5vMasvKwimGo3QIDAQAB" + pukRsa, err := PublicKeyRSA(publicKeyStr) + if err != nil { + t.Fatal(err) + } + got, err := Verify(signStr, signature, pukRsa) + if err != nil { + t.Fatal(err) + } + if !got { + t.Fatal("Verify() error = false") + } +} diff --git a/utils/sm/cipher.go b/utils/sm/cipher.go new file mode 100644 index 0000000..c82c3a5 --- /dev/null +++ b/utils/sm/cipher.go @@ -0,0 +1,29 @@ +package sm + +import ( + "crypto/rand" + "encoding/base64" + "github.com/tjfoc/gmsm/sm2" +) + +// Cipher 使用公钥加密数据 +func Cipher(publicKey *sm2.PublicKey, plaintext []byte) (string, error) { + ciphertext, err := sm2.Encrypt(publicKey, plaintext, rand.Reader, sm2.C1C2C3) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(ciphertext), nil +} + +// Plain 使用私钥解密数据 +func Plain(privateKey *sm2.PrivateKey, ciphertext string) (string, error) { + ciphertextBytes, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", err + } + decryptedText, err := sm2.Decrypt(privateKey, ciphertextBytes, sm2.C1C2C3) + if err != nil { + return "", err + } + return string(decryptedText), nil +} diff --git a/utils/sm/cipher_test.go b/utils/sm/cipher_test.go new file mode 100644 index 0000000..ce1754f --- /dev/null +++ b/utils/sm/cipher_test.go @@ -0,0 +1,33 @@ +package sm + +import ( + "testing" +) + +func TestCipher(t *testing.T) { + +} + +func TestPlain(t *testing.T) { + data := "123456{}测试" + prkStr := "zJRUcwPpKFf4nWiN9wqSO9gpGFx5BP4WviqnPsrhkpc=" + pukStr := "BKbxGVVlJGWK/ScU0ebKSe4Jr4LvcBGgvt/HHBk+ODVCYnJYvvmX8cDNpf3TVYuRdz/RUH6UDgcoVpz02jXNfrM=" + prk, err := PrivateKeySM(prkStr) + if err != nil { + t.Fatal(err) + } + puk, err := PublicKeySM(pukStr) + if err != nil { + t.Fatal(err) + } + cs, err := Cipher(puk, []byte(data)) + if err != nil { + t.Fatal(err) + } + t.Log(cs) + ps, err := Plain(prk, cs) + if err != nil { + t.Fatal(err) + } + t.Log(ps) +} diff --git a/utils/sm/generate.go b/utils/sm/generate.go new file mode 100644 index 0000000..91607ed --- /dev/null +++ b/utils/sm/generate.go @@ -0,0 +1,46 @@ +package sm + +import ( + "crypto/rand" + "encoding/base64" + "encoding/hex" + "fmt" + "github.com/tjfoc/gmsm/sm2" + "github.com/tjfoc/gmsm/x509" +) + +// GenerateKey 生成公钥和私钥 +func GenerateKey() (pri, puk string, err error) { + privateKey, err := sm2.GenerateKey(rand.Reader) + if err != nil { + return pri, puk, fmt.Errorf("秘钥生成失败:%v", err) + } + + priKeyBytes, err := hex.DecodeString(x509.WritePrivateKeyToHex(privateKey)) + pri = base64.StdEncoding.EncodeToString(priKeyBytes) + + publicKeyBytes, err := hex.DecodeString(x509.WritePublicKeyToHex(&privateKey.PublicKey)) + puk = base64.StdEncoding.EncodeToString(publicKeyBytes) + + return +} + +// GetPukByPrK 根据私钥得到公钥 +func GetPukByPrK(prk string) (string, error) { + privateKeyBytes, err := base64.StdEncoding.DecodeString(prk) + if err != nil { + return "", fmt.Errorf("私钥base64解码失败:%v", err) + } + + privateKey, err := x509.ReadPrivateKeyFromHex(hex.EncodeToString(privateKeyBytes)) + if err != nil { + return "", fmt.Errorf("私钥hex解码失败:%v", err) + } + + publicKeyBytes, err := hex.DecodeString(x509.WritePublicKeyToHex(&privateKey.PublicKey)) + if err != nil { + return "", fmt.Errorf("公钥hex解码失败:%v", err) + } + + return base64.StdEncoding.EncodeToString(publicKeyBytes), nil +} diff --git a/utils/sm/generate_test.go b/utils/sm/generate_test.go new file mode 100644 index 0000000..e4f4aa9 --- /dev/null +++ b/utils/sm/generate_test.go @@ -0,0 +1,28 @@ +package sm + +import ( + "fmt" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_GenerateKey(t *testing.T) { + priK, pubK, err := GenerateKey() + if err != nil { + t.Fatal(err) + } + t.Logf("\n私钥=%s\n公钥=%s\n", priK, pubK) +} + +// Test_GetPukByPrK 根据私钥获取公钥 +func Test_GetPukByPriK(t *testing.T) { + prk := "ZvMCTiG67qUyPN65fcgg+EHhy2W/fN+9ixBudcmfbuU=" + puk, err := GetPukByPrK(prk) + if err != nil { + t.Fatal(err) + } + want := "BL6PopnKr/hhPkxgn700Li1hPGx2/J5y2dQ4BDPLKDXe1sS4JeIG8/W1B8AO7hBzi0bKTArti0E/HJJcR9WcH/I=" + fmt.Printf("prk: %s \n", prk) + fmt.Printf("puk: %s \n", puk) + assert.Equal(t, puk, want) +} diff --git a/utils/sm/pem.go b/utils/sm/pem.go new file mode 100644 index 0000000..46c01f2 --- /dev/null +++ b/utils/sm/pem.go @@ -0,0 +1,37 @@ +package sm + +import ( + "encoding/base64" + "encoding/hex" + "fmt" + "github.com/tjfoc/gmsm/sm2" + "github.com/tjfoc/gmsm/x509" +) + +func PrivateKeySM(privateKeyStr string) (*sm2.PrivateKey, error) { + keyBytes, err := base64.StdEncoding.DecodeString(privateKeyStr) + if err != nil { + return nil, fmt.Errorf("privateKey base64 decode failed: %v", err) + } + + privateKey, err := x509.ReadPrivateKeyFromHex(hex.EncodeToString(keyBytes)) + if err != nil { + return nil, fmt.Errorf("read private key from hex failed: %v", err) + } + + return privateKey, nil +} + +func PublicKeySM(publicKeyStr string) (*sm2.PublicKey, error) { + keyBytes, err := base64.StdEncoding.DecodeString(publicKeyStr) + if err != nil { + return nil, fmt.Errorf("publicKeyStr base64 decode failed: %v", err) + } + + publicKey, err := x509.ReadPublicKeyFromHex(hex.EncodeToString(keyBytes)) + if err != nil { + return nil, fmt.Errorf("read public key from hex failed: %v", err) + } + + return publicKey, nil +} diff --git a/utils/sm/sign.go b/utils/sm/sign.go new file mode 100644 index 0000000..3a91f3b --- /dev/null +++ b/utils/sm/sign.go @@ -0,0 +1,28 @@ +package sm + +import ( + "crypto/rand" + "encoding/base64" + "fmt" + "github.com/tjfoc/gmsm/sm2" +) + +func Sign(data string, privateKey *sm2.PrivateKey) (string, error) { + signatureBytes, err := privateKey.Sign(rand.Reader, []byte(data), nil) + if err != nil { + return "", fmt.Errorf("sign failed: %v", err) + } + return base64.StdEncoding.EncodeToString(signatureBytes), nil +} + +func Verify(data string, signature string, publicKey *sm2.PublicKey) (bool, error) { + signatureBytes, err := base64.StdEncoding.DecodeString(signature) + if err != nil { + return false, fmt.Errorf("signature base64 decode failed: %v", err) + } + if !publicKey.Verify([]byte(data), signatureBytes) { + fmt.Println("Signature verification failed") + return false, nil + } + return true, nil +} diff --git a/utils/sm/sign_test.go b/utils/sm/sign_test.go new file mode 100644 index 0000000..6901326 --- /dev/null +++ b/utils/sm/sign_test.go @@ -0,0 +1,49 @@ +package sm + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_Sign(t *testing.T) { + data := "123456{}测试" + prkStr := "zJRUcwPpKFf4nWiN9wqSO9gpGFx5BP4WviqnPsrhkpc=" + prk, err := PrivateKeySM(prkStr) + if err != nil { + t.Fatal(err) + } + signature, err := Sign(data, prk) + if err != nil { + t.Fatal(err) + } + t.Logf("signature=%s\n", signature) +} + +func Test_Verify(t *testing.T) { + data := "123456{}测试" + prkStr := "zJRUcwPpKFf4nWiN9wqSO9gpGFx5BP4WviqnPsrhkpc=" + pukStr := "BKbxGVVlJGWK/ScU0ebKSe4Jr4LvcBGgvt/HHBk+ODVCYnJYvvmX8cDNpf3TVYuRdz/RUH6UDgcoVpz02jXNfrM=" + prk, err := PrivateKeySM(prkStr) + if err != nil { + t.Fatal(err) + } + puk, err := PublicKeySM(pukStr) + if err != nil { + t.Fatal(err) + } + signature, err := Sign(data, prk) + if err != nil { + t.Fatal(err) + } + t.Logf("signature=%s\n", signature) + + b, err := Verify(data, signature, puk) + if err != nil { + t.Fatal(err) + } + if assert.True(t, b) { + t.Logf("Test_sign 验签-成功 %t\n", b) + } else { + t.Errorf("Test_sign 验签-失败 %t\n", b) + } +} diff --git a/utils/sm/sm4.go b/utils/sm/sm4.go new file mode 100644 index 0000000..cbf091e --- /dev/null +++ b/utils/sm/sm4.go @@ -0,0 +1,77 @@ +package sm + +import ( + "crypto/cipher" + "crypto/rand" + "encoding/base64" + "github.com/tjfoc/gmsm/sm4" +) + +func GenerateSM4Key() (string, error) { + key := make([]byte, sm4.BlockSize) + _, err := rand.Read(key) + if err != nil { + return "", err + } + return base64.StdEncoding.EncodeToString(key), nil +} + +func Encode(key string, plaintextBytes []byte) (string, error) { + d, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return "", err + } + cipherBlock, err := sm4.NewCipher(d) + if err != nil { + return "", err + } + + blockSize := cipherBlock.BlockSize() + iv := make([]byte, blockSize) + for i := 0; i < blockSize; i++ { + iv[i] = 0 + } + blockMode := cipher.NewCBCEncrypter(cipherBlock, iv) + + padding := blockSize - len(plaintextBytes)%blockSize + for i := 0; i < padding; i++ { + plaintextBytes = append(plaintextBytes, byte(padding)) + } + cipherText := make([]byte, len(plaintextBytes)) + blockMode.CryptBlocks(cipherText, plaintextBytes) + + return base64.StdEncoding.EncodeToString(cipherText), nil +} + +func Decode(key, ciphertext string) (string, error) { + d, err := base64.StdEncoding.DecodeString(key) + if err != nil { + return "", err + } + + cipherBlock, err := sm4.NewCipher(d) + if err != nil { + return "", err + } + + blockSize := cipherBlock.BlockSize() + iv := make([]byte, blockSize) + for i := 0; i < blockSize; i++ { + iv[i] = 0 + } + + cipherTextBytes, err := base64.StdEncoding.DecodeString(ciphertext) + if err != nil { + return "", err + } + + plainText := make([]byte, len(cipherTextBytes)) + blockMode := cipher.NewCBCDecrypter(cipherBlock, iv) + blockMode.CryptBlocks(plainText, cipherTextBytes) + + plainTextLen := len(plainText) + padding := int(plainText[plainTextLen-1]) + buff := plainText[:plainTextLen-padding] + + return string(buff), nil +} diff --git a/utils/sm/sm4_test.go b/utils/sm/sm4_test.go new file mode 100644 index 0000000..0b3c698 --- /dev/null +++ b/utils/sm/sm4_test.go @@ -0,0 +1,53 @@ +package sm + +import ( + "testing" +) + +func TestSM4GenerateKey(t *testing.T) { + key, err := GenerateSM4Key() + if err != nil { + t.Error(err) + return + } + t.Log(key) +} + +func TestSM4(t *testing.T) { + key := "t+VxHnp+K9huhtNT84Pk7A==" + plaintextBytes := []byte("BZjU223ZBM7A8586Tm7P") + enc, err := Encode(key, plaintextBytes) + if err != nil { + t.Error(err) + return + } + t.Log(enc) + dec, err := Decode(key, enc) + if err != nil { + t.Error(err) + return + } + t.Log(dec) +} + +func TestSM4KeyEncrypt(t *testing.T) { + key := "t+VxHnp+K9huhtNT84Pk7A==" + plaintextBytes := []byte("BZjU223ZBM7A8586Tm7P") + enc, err := Encode(key, plaintextBytes) + if err != nil { + t.Error(err) + return + } + t.Log(enc) +} + +func TestSM4KeyPassDecrypt(t *testing.T) { + key := "t+VxHnp+K9huhtNT84Pk7A==" + ciphertext := "NwANcXkjX79873jenLJRGhbEr39eYOwC5WQxZFXmLpw=" + dec, err := Decode(key, ciphertext) + if err != nil { + t.Error(err) + return + } + t.Log(dec) +} diff --git a/utils/sort_struct.go b/utils/sort_struct.go new file mode 100644 index 0000000..c168c4b --- /dev/null +++ b/utils/sort_struct.go @@ -0,0 +1,45 @@ +package utils + +import ( + "reflect" + "sort" + "unicode" +) + +type KeyValue struct { + Key string + Value any +} + +func SortStruct(data any) []KeyValue { + v := reflect.ValueOf(data).Elem() + t := v.Type() + var kv []KeyValue + for i := 0; i < v.NumField(); i++ { + field := v.Field(i) + key := ToSnakeCase(t.Field(i).Name) + if field.IsZero() { + continue + } + kv = append(kv, KeyValue{Key: key, Value: field.Interface()}) + } + sort.SliceStable(kv, func(i, j int) bool { + return kv[i].Key < kv[j].Key + }) + return kv +} + +func ToSnakeCase(s string) string { + var result []rune + for i, char := range s { + if unicode.IsUpper(char) { + if i > 0 { + result = append(result, '_') + } + result = append(result, unicode.ToLower(char)) + } else { + result = append(result, char) + } + } + return string(result) +} diff --git a/utils/sort_struct_test.go b/utils/sort_struct_test.go new file mode 100644 index 0000000..aa195b5 --- /dev/null +++ b/utils/sort_struct_test.go @@ -0,0 +1,19 @@ +package utils + +import ( + "testing" +) + +func TestSortStruct(t *testing.T) { + data := &struct { + Name string `json:"name"` + Filed1 string `json:"filed1"` + Filed2 string `json:"filed2"` + }{ + Name: "test", + Filed1: "filed1", + Filed2: "filed2", + } + kv := SortStruct(data) + t.Log(kv) +}