diff --git a/docs/docs.go b/docs/docs.go index e4ae81d5..66cf12a4 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -1808,6 +1808,10 @@ const docTemplate = `{ "type": "string", "maxLength": 100 }, + "public_key": { + "type": "string", + "maxLength": 100 + }, "redirect_uri": { "type": "string", "maxLength": 100 @@ -1836,6 +1840,10 @@ const docTemplate = `{ "type": "string", "maxLength": 100 }, + "public_key": { + "type": "string", + "maxLength": 100 + }, "redirect_uri": { "type": "string", "maxLength": 100 diff --git a/docs/swagger.json b/docs/swagger.json index 969d5ada..21d54bfd 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -1799,6 +1799,10 @@ "type": "string", "maxLength": 100 }, + "public_key": { + "type": "string", + "maxLength": 100 + }, "redirect_uri": { "type": "string", "maxLength": 100 @@ -1827,6 +1831,10 @@ "type": "string", "maxLength": 100 }, + "public_key": { + "type": "string", + "maxLength": 100 + }, "redirect_uri": { "type": "string", "maxLength": 100 diff --git a/docs/swagger.yaml b/docs/swagger.yaml index ff85f3f9..e4d898dc 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -13,6 +13,9 @@ definitions: notify_url: maxLength: 100 type: string + public_key: + maxLength: 100 + type: string redirect_uri: maxLength: 100 type: string @@ -37,6 +40,9 @@ definitions: notify_url: maxLength: 100 type: string + public_key: + maxLength: 100 + type: string redirect_uri: maxLength: 100 type: string diff --git a/frontend/components/common/docs/api.tsx b/frontend/components/common/docs/api.tsx index ac6fa9d6..b30f45da 100644 --- a/frontend/components/common/docs/api.tsx +++ b/frontend/components/common/docs/api.tsx @@ -19,14 +19,113 @@ export const DOCS_LAST_UPDATED = "2026-01-07" export const apiSections: PolicySection[] = [ { value: "official-service", - title: "1. 官方服务接口", + title: "1. 官方 LDC 接口", content: (
-
-

官方服务接口暂未上线,敬请期待

+
+

官方原生接口,使用 Ed25519 签名算法,安全性更高

+
+ +

1.1 概览

+ + +

1.2 对接流程

+
    +
  1. 控制台创建应用,配置 client_id 并在应用设置中上传商户 Ed25519 公钥
  2. +
  3. 根据“签名算法”及商户私钥生成 sign
  4. +
  5. 调用 /pay/submit 发起积分流转请求
  6. +
  7. 认证完成后,通过异步回调或轮询接口同步状态
  8. +
+ +

1.3 鉴权与签名

+

1.3.1 签名算法

+
+
    +
  1. 取除 sign 以外的所有非空请求参数
  2. +
  3. 将参数按参数名 ASCII 码从到大排序(字典序)
  4. +
  5. 使用 k1=v1&k2=v2... 格式拼接成字符串
  6. +
  7. 应用密钥 (Client Secret) 直接追加到字符串末尾
  8. +
  9. 使用商户私钥对最终字符串进行 Ed25519 签名
  10. +
  11. 将签名结果转换成 Base64 编码作为 sign 参数
  12. +
+
+ +

1.4 积分流转服务

+ + + + + + 参数 + 必填 + 说明 + + + + + client_id + + 应用客户端 ID + + + type + + 固定 ldcpay + + + out_trade_no + + 业务单号 + + + money + + 积分数量,必须保留两位小数(比如,10.00) + + + order_name + + 商品名称 + + + sign + + 按“签名算法”生成的 Base64 签名串 + + + + +

1.5 其他接口

+

其他接口定义请参考 3. 其他接口

+
), + children: [ + { value: "1-1-overview", title: "1.1 概览" }, + { value: "1-2-flow", title: "1.2 对接流程" }, + { value: "1-3-auth-sign", title: "1.3 鉴权与签名" }, + { value: "1-4-submit", title: "1.4 积分流转服务" }, + { value: "1-5-others", title: "1.5 其他接口" }, + ] }, { value: "epay-compatibility", @@ -90,7 +189,7 @@ sign=$(echo -n "\${payload}\${SECRET}" | md5) # 输出小写`}

2.5 积分流转服务

@@ -171,7 +270,34 @@ sign=$(echo -n "\${payload}\${SECRET}" | md5) # 输出小写`} language="bash" /> -

2.6 订单查询

+

2.6 其他接口

+

其他接口定义请参考 3. 其他接口

+ +
+ ), + children: [ + { value: "2-1-overview", title: "2.1 概览" }, + { value: "2-2-common-errors", title: "2.2 常见错误" }, + { value: "2-3-flow", title: "2.3 对接流程" }, + { value: "2-4-auth-sign", title: "2.4 鉴权与签名" }, + { value: "2-5-submit", title: "2.5 积分流转服务" }, + { value: "2-6-others", title: "2.6 其他接口" }, + ] + }, + { + value: "common-services", + title: "3. 其他接口", + content: ( +
+
+

官方接口与易支付兼容接口公用接口。

+
+ +

3.1 订单查询

应用需返回 HTTP 200 且响应体为 success(大小写不敏感),否则视为失败并继续重试。

+ +

3.4 商户分发接口

+ + + + + + 参数 + 必填 + 说明 + + + + + user_id + + 收款人用户 ID (数字) + + + username + + 收款人用户名 (用于二次校验) + + + amount + + 分发积分数量,最多 2 位小数 + + + out_trade_no + + 商户自定义单号 + + + remark + + 分发备注 + + + +

成功响应:{`{"code":1, "data":{"trade_no":"...", "out_trade_no":"..."}}`}

), children: [ - { value: "2-1-overview", title: "2.1 概览" }, - { value: "2-2-common-errors", title: "2.2 常见错误" }, - { value: "2-3-flow", title: "2.3 对接流程" }, - { value: "2-4-auth-sign", title: "2.4 鉴权与签名" }, - { value: "2-5-submit", title: "2.5 积分流转服务" }, - { value: "2-6-order", title: "2.6 订单查询" }, - { value: "2-7-refund", title: "2.7 订单退款" }, - { value: "2-8-notify", title: "2.8 异步通知" }, + { value: "3-1-order", title: "3.1 订单查询" }, + { value: "3-2-refund", title: "3.2 订单退款" }, + { value: "3-3-notify", title: "3.3 异步通知" }, + { value: "3-4-distribute", title: "3.4 商户分发接口" }, ] }, ] diff --git a/frontend/components/common/merchant/merchant-dialog.tsx b/frontend/components/common/merchant/merchant-dialog.tsx index 1e67410d..9d124ade 100644 --- a/frontend/components/common/merchant/merchant-dialog.tsx +++ b/frontend/components/common/merchant/merchant-dialog.tsx @@ -53,6 +53,7 @@ export function MerchantDialog({ app_description: '', redirect_uri: '', notify_url: '', + public_key: '', test_mode: false, }) @@ -71,6 +72,7 @@ export function MerchantDialog({ app_description: apiKey.app_description, redirect_uri: apiKey.redirect_uri, notify_url: apiKey.notify_url, + public_key: apiKey.public_key, test_mode: apiKey.test_mode, } } @@ -177,8 +179,8 @@ export function MerchantDialog({ setOpen(false) } catch (error) { - const errorMessage = (error as Error).message || `无法${ mode === 'create' ? '创建' : '更新' }应用` - toast.error(`${ mode === 'create' ? '创建' : '更新' }失败`, { + const errorMessage = (error as Error).message || `无法${mode === 'create' ? '创建' : '更新'}应用` + toast.error(`${mode === 'create' ? '创建' : '更新'}失败`, { description: errorMessage }) throw error @@ -329,6 +331,18 @@ export function MerchantDialog({ disabled={processing} /> + +
+ + setFormData({ ...formData, public_key: e.target.value })} + disabled={processing} + /> +
diff --git a/frontend/lib/services/merchant/types.ts b/frontend/lib/services/merchant/types.ts index 322c37a4..cf395d2e 100644 --- a/frontend/lib/services/merchant/types.ts +++ b/frontend/lib/services/merchant/types.ts @@ -20,6 +20,8 @@ export interface MerchantAPIKey { redirect_uri?: string; /** 通知 URL */ notify_url: string; + /** 公钥 (Base64) */ + public_key?: string; /** 测试模式 */ test_mode: boolean; /** 创建时间 */ @@ -44,6 +46,8 @@ export interface CreateAPIKeyRequest { redirect_uri?: string; /** 通知 URL(最大100字符,必须是有效的 URL) */ notify_url: string; + /** 公钥 (Base64 编码,32字节,可选) */ + public_key?: string; /** 测试模式(可选,默认为 false) */ test_mode?: boolean; } @@ -62,6 +66,8 @@ export interface UpdateAPIKeyRequest { redirect_uri?: string; /** 通知 URL(最大100字符,必须是有效的 URL,可选) */ notify_url?: string; + /** 公钥 (Base64 编码,32字节,可选) */ + public_key?: string; /** 测试模式(可选) */ test_mode?: boolean; } diff --git a/internal/apps/merchant/api_key/routers.go b/internal/apps/merchant/api_key/routers.go index 314e0909..dc6cc64a 100644 --- a/internal/apps/merchant/api_key/routers.go +++ b/internal/apps/merchant/api_key/routers.go @@ -22,6 +22,7 @@ import ( "github.com/gin-gonic/gin" "github.com/linux-do/credit/internal/apps/merchant" "github.com/linux-do/credit/internal/apps/oauth" + "github.com/linux-do/credit/internal/apps/payment" "github.com/linux-do/credit/internal/db" "github.com/linux-do/credit/internal/model" "github.com/linux-do/credit/internal/util" @@ -33,6 +34,7 @@ type CreateAPIKeyRequest struct { AppDescription string `json:"app_description" binding:"max=100"` RedirectURI string `json:"redirect_uri" binding:"omitempty,max=100,url"` NotifyURL string `json:"notify_url" binding:"required,max=100,url"` + PublicKey string `json:"public_key" binding:"omitempty,max=100"` TestMode bool `json:"test_mode"` } @@ -42,6 +44,7 @@ type UpdateAPIKeyRequest struct { AppDescription string `json:"app_description" binding:"omitempty,max=100"` RedirectURI string `json:"redirect_uri" binding:"omitempty,max=100,url"` NotifyURL string `json:"notify_url" binding:"omitempty,max=100,url"` + PublicKey string `json:"public_key" binding:"omitempty,max=100"` TestMode bool `json:"test_mode"` } @@ -78,6 +81,19 @@ func CreateAPIKey(c *gin.Context) { TestMode: req.TestMode, } + if len(req.PublicKey) > 0 { + publicKeyBytes, err := util.Base64Decode(req.PublicKey) + if err != nil { + c.JSON(http.StatusBadRequest, util.Err(payment.InvalidPublicKeyFormat)) + return + } + if len(publicKeyBytes) != 32 { + c.JSON(http.StatusBadRequest, util.Err(payment.InvalidPublicKeyLength)) + return + } + apiKey.PublicKey = publicKeyBytes + } + if err := db.DB(c.Request.Context()).Create(&apiKey).Error; err != nil { c.JSON(http.StatusInternalServerError, util.Err(err.Error())) return @@ -143,6 +159,19 @@ func UpdateAPIKey(c *gin.Context) { "test_mode": req.TestMode, } + if len(req.PublicKey) > 0 { + publicKeyBytes, err := util.Base64Decode(req.PublicKey) + if err != nil { + c.JSON(http.StatusBadRequest, util.Err(payment.InvalidPublicKeyFormat)) + return + } + if len(publicKeyBytes) != 32 { + c.JSON(http.StatusBadRequest, util.Err(payment.InvalidPublicKeyLength)) + return + } + updates["public_key"] = publicKeyBytes + } + if err := db.DB(c.Request.Context()). Model(&apiKey). Updates(updates).Error; err != nil { diff --git a/internal/apps/payment/errs.go b/internal/apps/payment/errs.go index d55e0859..f082b238 100644 --- a/internal/apps/payment/errs.go +++ b/internal/apps/payment/errs.go @@ -17,12 +17,14 @@ limitations under the License. package payment const ( - OrderNotFound = "订单不存在或已完成" - OrderStatusInvalid = "订单状态不允许支付" - OrderExpired = "订单已过期" - MerchantInfoNotFound = "商户信息不存在" - RecipientNotFound = "收款人不存在" - OrderNoFormatError = "订单号格式错误" - CannotTransferToSelf = "不能转账给自己" - PayConfigNotFound = "支付配置不存在" + OrderNotFound = "订单不存在或已完成" + OrderStatusInvalid = "订单状态不允许支付" + OrderExpired = "订单已过期" + MerchantInfoNotFound = "商户信息不存在" + RecipientNotFound = "收款人不存在" + OrderNoFormatError = "订单号格式错误" + CannotTransferToSelf = "不能转账给自己" + PayConfigNotFound = "支付配置不存在" + InvalidPublicKeyFormat = "公钥格式错误" + InvalidPublicKeyLength = "公钥长度必须为32字节" ) diff --git a/internal/apps/payment/middlewares.go b/internal/apps/payment/middlewares.go index ece7a35b..8dbfa227 100644 --- a/internal/apps/payment/middlewares.go +++ b/internal/apps/payment/middlewares.go @@ -52,13 +52,23 @@ type EPayRequest struct { SignType string `form:"sign_type"` } -// ToCreateOrderRequest 转换为通用创建订单请求 -func (r *EPayRequest) ToCreateOrderRequest() *CreateOrderRequest { +// LDCPayRequest LDC支付请求 +type LDCPayRequest struct { + ClientID string `form:"client_id" binding:"required"` + OrderName string `form:"order_name" binding:"required,max=64"` + MerchantOrderNo *string `form:"out_trade_no" binding:"required,min=1,max=64"` + Amount decimal.Decimal `form:"money" binding:"required"` + PayType string `form:"type" binding:"required"` + Sign string `form:"sign" binding:"required"` +} + +// NewCreateOrderRequest 从支付请求创建通用订单请求 +func NewCreateOrderRequest(orderName string, merchantOrderNo *string, amount decimal.Decimal, payType string) *CreateOrderRequest { return &CreateOrderRequest{ - OrderName: r.OrderName, - MerchantOrderNo: r.MerchantOrderNo, - Amount: r.Amount, - PaymentType: r.PayType, + OrderName: orderName, + MerchantOrderNo: merchantOrderNo, + Amount: amount, + PaymentType: payType, } } @@ -116,20 +126,28 @@ func RequireSignatureAuth() gin.HandlerFunc { PayType := c.Request.FormValue("type") var apiKey model.MerchantAPIKey + var createOrderReq *CreateOrderRequest + var err error switch PayType { + case common.PayTypeLDCPay: + createOrderReq, err = VerifySignatureEd25519(c, &apiKey) + if err != nil { + c.AbortWithStatusJSON(http.StatusUnauthorized, util.Err(err.Error())) + return + } case common.PayTypeEPay: - if createOrderReq, err := VerifySignature(c, &apiKey); err != nil { + createOrderReq, err = VerifySignatureMD5(c, &apiKey) + if err != nil { c.AbortWithStatusJSON(http.StatusUnauthorized, util.Err(err.Error())) return - } else { - util.SetToContext(c, CreateOrderRequestKey, createOrderReq) } default: c.AbortWithStatusJSON(http.StatusBadRequest, util.Err("不支持的请求类型")) return } + util.SetToContext(c, CreateOrderRequestKey, createOrderReq) util.SetToContext(c, APIKeyObjKey, &apiKey) c.Next() diff --git a/internal/apps/payment/tasks.go b/internal/apps/payment/tasks.go index 5cbf5a0b..0a643e2b 100644 --- a/internal/apps/payment/tasks.go +++ b/internal/apps/payment/tasks.go @@ -74,10 +74,9 @@ func HandleMerchantPaymentNotify(ctx context.Context, t *asynq.Task) error { "name": order.OrderName, "money": order.Amount.Truncate(2).StringFixed(2), "trade_status": "TRADE_SUCCESS", - "sign_type": "MD5", } - callbackParams["sign"] = GenerateSignature(callbackParams, apiKey.ClientSecret) + callbackParams["sign"] = GenerateSignature(callbackParams, apiKey.ClientSecret, true) if err := sendCallbackRequest(ctx, apiKey.NotifyURL, callbackParams); err != nil { retried, _ := asynq.GetRetryCount(ctx) diff --git a/internal/apps/payment/utils.go b/internal/apps/payment/utils.go index 65aadaa5..1aab9d90 100644 --- a/internal/apps/payment/utils.go +++ b/internal/apps/payment/utils.go @@ -150,8 +150,8 @@ func ParseOrderNo(c *gin.Context, orderNo string) (*OrderContext, error) { return ctx, nil } -// GenerateSignature 生成MD5签名 -func GenerateSignature(params map[string]string, secret string) string { +// GenerateSignature 生成签名 +func GenerateSignature(params map[string]string, secret string, isMD5 bool) string { // 按key排序 keys := make([]string, 0, len(params)) for k := range params { @@ -180,13 +180,16 @@ func GenerateSignature(params map[string]string, secret string) string { } builder.WriteString(secret) - // MD5加密 - hash := md5.Sum([]byte(builder.String())) - return fmt.Sprintf("%x", hash) + if isMD5 { + // MD5加密 + hash := md5.Sum([]byte(builder.String())) + return fmt.Sprintf("%x", hash) + } + return builder.String() } -// VerifySignature 验证MD5签名 -func VerifySignature(c *gin.Context, apiKey *model.MerchantAPIKey) (*CreateOrderRequest, error) { +// VerifySignatureMD5 验证MD5签名 +func VerifySignatureMD5(c *gin.Context, apiKey *model.MerchantAPIKey) (*CreateOrderRequest, error) { var req EPayRequest if err := c.ShouldBind(&req); err != nil { return nil, err @@ -213,10 +216,10 @@ func VerifySignature(c *gin.Context, apiKey *model.MerchantAPIKey) (*CreateOrder } params["money"] = req.Amount.Truncate(2).StringFixed(2) - expectedSignFixed := GenerateSignature(params, apiKey.ClientSecret) + expectedSignFixed := GenerateSignature(params, apiKey.ClientSecret, true) params["money"] = req.Amount.Truncate(2).String() - expectedSignTrimmed := GenerateSignature(params, apiKey.ClientSecret) + expectedSignTrimmed := GenerateSignature(params, apiKey.ClientSecret, true) matchFixed := subtle.ConstantTimeCompare([]byte(strings.ToLower(expectedSignFixed)), []byte(strings.ToLower(req.Sign))) == 1 matchTrimmed := subtle.ConstantTimeCompare([]byte(strings.ToLower(expectedSignTrimmed)), []byte(strings.ToLower(req.Sign))) == 1 @@ -225,5 +228,49 @@ func VerifySignature(c *gin.Context, apiKey *model.MerchantAPIKey) (*CreateOrder return nil, errors.New("签名验证失败") } - return req.ToCreateOrderRequest(), nil + return NewCreateOrderRequest(req.OrderName, req.MerchantOrderNo, req.Amount, req.PayType), nil +} + +// VerifySignatureEd25519 验证 Ed25519 签名 +func VerifySignatureEd25519(c *gin.Context, apiKey *model.MerchantAPIKey) (*CreateOrderRequest, error) { + var req LDCPayRequest + if err := c.ShouldBind(&req); err != nil { + return nil, err + } + + // 验证金额 + if err := util.ValidateAmount(req.Amount); err != nil { + return nil, err + } + + if err := apiKey.GetByClientID(db.DB(c.Request.Context()), req.ClientID); err != nil { + return nil, err + } + + if len(apiKey.PublicKey) == 0 { + return nil, errors.New("商户未配置公钥") + } + + signatureBytes, err := util.Base64Decode(req.Sign) + if err != nil { + return nil, errors.New("签名格式错误") + } + + // 构建签名参数 + params := map[string]string{ + "client_id": req.ClientID, + "type": req.PayType, + "out_trade_no": util.DerefString(req.MerchantOrderNo), + "order_name": req.OrderName, + "money": req.Amount.Truncate(2).StringFixed(2), + } + + signatureParam := GenerateSignature(params, apiKey.ClientSecret, false) + validTrimmed := util.Ed25519Verify(apiKey.PublicKey, []byte(signatureParam), signatureBytes) + + if !validTrimmed { + return nil, errors.New("签名验证失败") + } + + return NewCreateOrderRequest(req.OrderName, req.MerchantOrderNo, req.Amount, req.PayType), nil } diff --git a/internal/common/constants.go b/internal/common/constants.go index 1456d6b9..118f7c5d 100644 --- a/internal/common/constants.go +++ b/internal/common/constants.go @@ -17,8 +17,8 @@ limitations under the License. package common const ( - // PayTypeLDPay Linux Do Credit 支付类型标识 - PayTypeLDPay = "ldpay" + // PayTypeLDCPay Linux Do Credit 支付类型标识 + PayTypeLDCPay = "ldcpay" // PayTypeEPay Epay 支付类型 PayTypeEPay = "epay" ) diff --git a/internal/model/merchant_api_keys.go b/internal/model/merchant_api_keys.go index dd5aaff3..f54406af 100644 --- a/internal/model/merchant_api_keys.go +++ b/internal/model/merchant_api_keys.go @@ -33,6 +33,7 @@ type MerchantAPIKey struct { AppDescription string `json:"app_description" gorm:"size:100"` RedirectURI string `json:"redirect_uri" gorm:"size:100"` NotifyURL string `json:"notify_url" gorm:"size:100;not null"` + PublicKey []byte `json:"public_key" gorm:"type:bytea"` TestMode bool `json:"test_mode" gorm:"default:false"` CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_merchant_api_keys_user_created,priority:2"` UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime"` diff --git a/internal/model/orders.go b/internal/model/orders.go index bda870d0..cf36d9ad 100644 --- a/internal/model/orders.go +++ b/internal/model/orders.go @@ -67,13 +67,13 @@ type Order struct { PayerUsername string `json:"payer_username" gorm:"-:migration;->"` PayeeUsername string `json:"payee_username" gorm:"-:migration;->"` Amount decimal.Decimal `json:"amount" gorm:"type:numeric(20,2);not null;index"` - Status OrderStatus `json:"status" gorm:"type:varchar(20);not null;index:idx_orders_payee_status_type_created,priority:2;index:idx_orders_payer_status_type_created,priority:2;index:idx_orders_client_status_created,priority:2;index:idx_orders_payer_status_type_trade,priority:2;index:idx_orders_payment_link_status,priority:2"` + Status OrderStatus `json:"status" gorm:"type:varchar(20);not null;index:idx_orders_payee_status_type_created,priority:2;index:idx_orders_payer_status_type_created,priority:2;index:idx_orders_client_status_created,priority:2;index:idx_orders_payer_status_type_trade,priority:2;index:idx_orders_payment_link_status,priority:2;index:idx_orders_status_expires,priority:1"` Type OrderType `json:"type" gorm:"type:varchar(20);not null;index:idx_orders_payee_status_type_created,priority:3;index:idx_orders_payer_status_type_created,priority:3;index:idx_orders_payer_status_type_trade,priority:3"` Remark string `json:"remark" gorm:"size:255"` PaymentType string `json:"payment_type" gorm:"size:20"` PaymentLinkID *uint64 `json:"payment_link_id,string" gorm:"index:idx_orders_payment_link_status,priority:1"` TradeTime time.Time `json:"trade_time" gorm:"index:idx_orders_payer_status_type_trade,priority:4"` - ExpiresAt time.Time `json:"expires_at" gorm:"not null"` + ExpiresAt time.Time `json:"expires_at" gorm:"not null;index:idx_orders_status_expires,priority:2"` CreatedAt time.Time `json:"created_at" gorm:"autoCreateTime;index:idx_orders_payee_status_type_created,priority:4;index:idx_orders_payer_status_type_created,priority:4;index:idx_orders_client_status_created,priority:3"` UpdatedAt time.Time `json:"updated_at" gorm:"autoUpdateTime;index"` } diff --git a/internal/util/crypto.go b/internal/util/crypto.go index 822dca4c..5f055e84 100644 --- a/internal/util/crypto.go +++ b/internal/util/crypto.go @@ -19,6 +19,7 @@ package util import ( "crypto/aes" "crypto/cipher" + "crypto/ed25519" "crypto/rand" "encoding/base64" "encoding/hex" @@ -80,7 +81,7 @@ func encryptBytes(signKey string, plaintext []byte) (string, error) { ciphertext := gcm.Seal(nonce, nonce, plaintext, nil) // 返回 base64 编码的密文 - return base64.StdEncoding.EncodeToString(ciphertext), nil + return Base64Encode(ciphertext), nil } // decryptBytes 解密函数,处理字节数据 @@ -95,7 +96,7 @@ func decryptBytes(signKey string, ciphertext string) ([]byte, error) { } // 解码 base64 密文 - data, err := base64.StdEncoding.DecodeString(ciphertext) + data, err := Base64Decode(ciphertext) if err != nil { return nil, fmt.Errorf("failed to decode ciphertext: %w", err) } @@ -128,3 +129,30 @@ func decryptBytes(signKey string, ciphertext string) ([]byte, error) { return plaintext, nil } + +// Base64Encode Base64编码 +func Base64Encode(data []byte) string { + return base64.StdEncoding.EncodeToString(data) +} + +// Base64Decode Base64解码 +func Base64Decode(encoded string) ([]byte, error) { + return base64.StdEncoding.DecodeString(encoded) +} + +// Ed25519Verify 验证 Ed25519 签名 +// publicKey: 32 字节的公钥(已解码的二进制格式) +// message: 待验证的原始消息 +// signature: 64 字节的签名(已解码的二进制格式) +// return: 签名是否有效 +func Ed25519Verify(publicKey, message, signature []byte) bool { + if len(publicKey) != ed25519.PublicKeySize { + return false + } + + if len(signature) != ed25519.SignatureSize { + return false + } + + return ed25519.Verify(publicKey, message, signature) +}