Skip to content

Commit

Permalink
feat: 增加了对普通推送 API 的 SM2 加密推送功能。
Browse files Browse the repository at this point in the history
  • Loading branch information
calvinit committed Jan 15, 2025
1 parent 727cc0d commit b24b737
Show file tree
Hide file tree
Showing 17 changed files with 2,682 additions and 15 deletions.
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

## [v0.1.2](https://github.com/calvinit/jiguang-sdk-go/releases/tag/v0.1.2) - 2025-01-15

### 新特性

- 增加了对普通推送 API 的 SM2 加密推送功能。

---

## [v0.1.1](https://github.com/calvinit/jiguang-sdk-go/releases/tag/v0.1.1) - 2024-12-31

### 修复
Expand Down
10 changes: 8 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@

`jiguang-sdk-go` 是基于 Go 的极光 REST API
封装开发包,参考了极光官方提供的 [jiguang-sdk-java](https://github.com/jpush/jiguang-sdk-java) 实现。
它致力于为开发者提供便捷、高效的服务端集成方式,并支持最新的 API 功能。
它致力于为开发者提供便捷、轻量和高效的服务端集成方式,并支持最新的 API 功能。

### 特性

Expand Down Expand Up @@ -129,7 +129,13 @@
---
## 七、参考链接
## 七、致谢
- 感谢 [tjfoc/gmsm](https://github.com/tjfoc/gmsm) 库提供的支持,帮助我们实现了 SM2 加解密算法的核心功能。
---
## 八、参考链接
- [jiguang-sdk-java](https://github.com/jpush/jiguang-sdk-java)
- [极光文档中心](https://docs.jiguang.cn)
6 changes: 6 additions & 0 deletions api/jpush/push/v3.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,12 @@ type APIv3 interface {
// - 接口文档:https://docs.jiguang.cn/jpush/server/push/rest_api_v3_push
Send(ctx context.Context, param *SendParam) (*SendResult, error)

// 普通推送(SM2 加密)
// - 功能说明:向某单个设备或者某设备列表推送一条通知或者消息。推送的内容只能是 JSON 表示的一个推送对象。
// - 调用地址:POST `/v3/push`
// - 接口文档:https://docs.jiguang.cn/jpush/server/push/rest_api_v3_push
SendWithSM2(ctx context.Context, param *SendParam) (*SendResult, error)

// 文件推送(文件立即推送)
// - 功能说明:支持指定文件唯一标识(fileID)进行推送,文件唯一标识(fileID)可以参考 File API v3 的 文件上传接口 获得。
// - 调用地址:POST `/v3/push/file`
Expand Down
51 changes: 51 additions & 0 deletions api/jpush/push/v3_send.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ import (
"net/http"

"github.com/calvinit/jiguang-sdk-go/api"
"github.com/calvinit/jiguang-sdk-go/jiguang"
)

// 普通推送
Expand Down Expand Up @@ -65,6 +66,56 @@ func (p *apiv3) CustomSend(ctx context.Context, param interface{}) (*SendResult,
return result, nil
}

// 普通推送(SM2 加密)
// - 功能说明:向某单个设备或者某设备列表推送一条通知或者消息。推送的内容只能是 JSON 表示的一个推送对象。
// - 调用地址:POST `/v3/push`
// - 接口文档:https://docs.jiguang.cn/jpush/server/push/rest_api_v3_push
func (p *apiv3) SendWithSM2(ctx context.Context, param *SendParam) (*SendResult, error) {
if p == nil {
return nil, api.ErrNilJPushPushAPIv3
}

if param == nil {
return nil, errors.New("`param` cannot be nil")
}

original, err := json.Marshal(param)
if err != nil {
return nil, err
}
payload, err := jiguang.EncryptWithSM2(original)
if err != nil {
return nil, err
}
sm2PushParam := &sm2Push{Audience: param.Audience, Payload: payload}

req := &api.Request{
Method: http.MethodPost,
Proto: p.proto,
URL: p.host + "/v3/push",
Auth: p.auth,
Header: http.Header{"X-Encrypt-Type": {"SM2"}},
Body: sm2PushParam,
}
resp, err := p.client.Request(ctx, req)
if err != nil {
return nil, err
}

result := &SendResult{Response: resp}
err = json.Unmarshal(resp.RawBody, result)
if err != nil {
return nil, err
}
return result, nil
}

// SM2 加密推送参数
type sm2Push struct {
Audience interface{} `json:"audience"` // 推送目标,同 SendParam.Audience
Payload string `json:"payload"` // 推送内容,SendParam 的 JSON 字符串使用 SM2 公钥加密后的密文(Base64 编码)
}

type SendResult struct {
*api.Response `json:"-"`
Error *api.CodeError `json:"error,omitempty"`
Expand Down
9 changes: 9 additions & 0 deletions api/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ type Request struct {
Proto string // 协议版本,如 "HTTP/1.0"、"HTTP/1.1"、"HTTP/2.0" 等。
URL string // 请求完整 URL
Auth string // 请求授权信息
Header http.Header // 自定义请求头
Body interface{} // 请求正文负载
}

Expand All @@ -56,6 +57,10 @@ func newApplicationJSONRequest(ctx context.Context, req *Request) (*http.Request
httpReq.Proto = req.Proto
}

for k, v := range req.Header {
httpReq.Header[k] = v
}

httpReq.Header.Set("Authorization", req.Auth)
httpReq.Header.Set("Content-Type", "application/json;charset=UTF-8")
httpReq.Header.Set("User-Agent", defaultUserAgent)
Expand Down Expand Up @@ -83,6 +88,10 @@ func newMultipartFormDataRequest(ctx context.Context, req *Request) (*http.Reque
httpReq.Proto = req.Proto
}

for k, v := range req.Header {
httpReq.Header[k] = v
}

httpReq.Header.Set("Authorization", req.Auth)
httpReq.Header.Set("Content-Type", writer.FormDataContentType())
httpReq.Header.Set("User-Agent", defaultUserAgent)
Expand Down
2 changes: 1 addition & 1 deletion examples/adapter/logger_zerolog.go
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ func NewZeroLogger() *ZeroLogger {

// colorize returns the string s wrapped in ANSI code c.
func colorize(s interface{}, c int) string {
return fmt.Sprintf("\x1b[%dm%v\x1b[0m", c, s)
return fmt.Sprintf("\x1b[%dm%-5s\x1b[0m", c, s)
}

func (z *ZeroLogger) Debug(_ context.Context, msg string) {
Expand Down
9 changes: 3 additions & 6 deletions examples/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,11 @@ module github.com/calvinit/jiguang-sdk-go/examples

go 1.16

retract (
v0.0.0-20241231094233-e49f35b049d0
v0.0.0-20241231093211-6c13dbb3190d
)
retract v0.0.0-20241231131241-727cc0d7c5d9

require (
github.com/calvinit/jiguang-sdk-go v0.1.1
github.com/go-resty/resty/v2 v2.16.2
github.com/calvinit/jiguang-sdk-go v0.1.2
github.com/go-resty/resty/v2 v2.16.3
github.com/rs/zerolog v1.33.0
github.com/sirupsen/logrus v1.9.3
go.uber.org/zap v1.24.0 // It's the latest version that supports go 1.16.
Expand Down
6 changes: 2 additions & 4 deletions examples/go.sum
Original file line number Diff line number Diff line change
@@ -1,13 +1,11 @@
github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
github.com/calvinit/jiguang-sdk-go v0.1.0 h1:B3BG/fw6kd0Y+NYbKdiM3kPdsH1unaJ6ao5T0KxXggw=
github.com/calvinit/jiguang-sdk-go v0.1.0/go.mod h1:s2WAdYC1aU85kNCPKUjNgIPQ4QsfRokIZoSzq6qtTfM=
github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
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/go-resty/resty/v2 v2.16.2 h1:CpRqTjIzq/rweXUt9+GxzzQdlkqMdt8Lm/fuK/CAbAg=
github.com/go-resty/resty/v2 v2.16.2/go.mod h1:0fHAoK7JoBy/Ch36N8VFeMsK7xQOHhvWaC3iOktwmIU=
github.com/go-resty/resty/v2 v2.16.3 h1:zacNT7lt4b8M/io2Ahj6yPypL7bqx9n1iprfQuodV+E=
github.com/go-resty/resty/v2 v2.16.3/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA=
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
Expand Down
1 change: 1 addition & 0 deletions examples/jpush/pushv3_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ func TestPushAPIv3_Send(t *testing.T) {
param.Callback = callback

result, err := pushAPIv3.Send(context.Background(), param)
// result, err := pushAPIv3.SendWithSM2(context.Background(), param)
// result, err := pushAPIv3.CustomSend(context.Background(), param)
if err != nil {
t.Fatalf("Failed! Error: %s", err)
Expand Down
2 changes: 1 addition & 1 deletion examples/version.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,4 @@
package examples

// 当前极光 SDK 使用范例模块的版本号。
const Version = "v0.1.1"
const Version = "v0.1.2"
126 changes: 126 additions & 0 deletions jiguang/gmsm2.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
/*
*
* Copyright 2025 calvinit/jiguang-sdk-go authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
*/

package jiguang

import (
"crypto/elliptic"
"crypto/rand"
"encoding/base64"
"errors"
"fmt"
"math/big"

"github.com/calvinit/jiguang-sdk-go/third_party/gmsm/sm2"
)

const (
sm2B64PubKey = "BPj6Mj/T444gxPaHc6CDCizMRp4pEl14WI2lvIbdEK2c+5XiSqmQt2TQc8hMMZqfxcDqUNQW95puAfQx1asv3rU="
sm2B64PrivKey = "EjRWeJA=" // N/A: 在客户端 SDK 中不会使用私钥,因此不提供私钥,私钥应该由极光服务端保存。
)

var (
sm2PubKey *sm2.PublicKey
sm2PrivKey *sm2.PrivateKey
)

func init() {
var err error

// 初始化 SM2 公钥
sm2PubKey, err = desm2PubKey(sm2B64PubKey)
if err != nil {
panic(fmt.Sprintf("failed to initialize SM2 public key: %v", err))
}

// 初始化 SM2 私钥
sm2PrivKey, err = desm2PrivKey(sm2B64PrivKey)
if err != nil {
panic(fmt.Sprintf("failed to initialize SM2 private key: %v", err))
}
}

// 从 Base64 编码解析 SM2 公钥。
func desm2PubKey(b64PubKey string) (*sm2.PublicKey, error) {
pubKeyBytes, err := base64.StdEncoding.DecodeString(b64PubKey)
if err != nil {
return nil, fmt.Errorf("failed to decode Base64 SM2 public key: %w", err)
}

curve := sm2.P256Sm2() // 使用 sm2p256v1 曲线
x, y := elliptic.Unmarshal(curve, pubKeyBytes) // 反序列化为椭圆曲线点
if x == nil || y == nil {
return nil, errors.New("invalid SM2 public key: unable to unmarshal curve point")
}

// 返回解析后的公钥
return &sm2.PublicKey{
Curve: curve,
X: x,
Y: y,
}, nil
}

// 从 Base64 编码解析 SM2 私钥。
func desm2PrivKey(b64PrivKey string) (*sm2.PrivateKey, error) {
privKeyBytes, err := base64.StdEncoding.DecodeString(b64PrivKey)
if err != nil {
return nil, fmt.Errorf("failed to decode Base64 SM2 private key: %w", err)
}

curve := sm2.P256Sm2() // 使用 sm2p256v1 曲线
privKey := new(sm2.PrivateKey)
privKey.D = new(big.Int).SetBytes(privKeyBytes) // 将字节数组转换为大整数
privKey.PublicKey.Curve = curve // 设置公钥曲线
privKey.PublicKey.X, privKey.PublicKey.Y = curve.ScalarBaseMult(privKeyBytes) // 计算公钥坐标

return privKey, nil
}

// 使用 SM2 公钥加密数据并返回 Base64 编码字符串。
func EncryptWithSM2(data []byte) (string, error) {
if sm2PubKey == nil {
return "", errors.New("SM2 public key not initialized")
}

cipherBytes, err := sm2.Encrypt(sm2PubKey, data, rand.Reader, sm2.C1C2C3)
if err != nil {
return "", fmt.Errorf("SM2 encryption error: %w", err)
}

return base64.StdEncoding.EncodeToString(cipherBytes), nil
}

// 使用 SM2 私钥解密 Base64 编码的加密数据。
func DecryptWithSM2(data string) ([]byte, error) {
if sm2PrivKey == nil {
return nil, errors.New("SM2 private key not initialized")
}

cipherBytes, err := base64.StdEncoding.DecodeString(data)
if err != nil {
return nil, fmt.Errorf("failed to decode data: %w", err)
}

plainBytes, err := sm2.Decrypt(sm2PrivKey, cipherBytes, sm2.C1C2C3)
if err != nil {
return nil, fmt.Errorf("SM2 decryption error: %w", err)
}

return plainBytes, nil
}
Loading

0 comments on commit b24b737

Please sign in to comment.