DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go): - GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面 - GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新 - 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新) - 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍 - 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0) - 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算 启动器(Python/JS): - 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭 - GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐) - 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
This commit is contained in:
commit
645b64fe21
4388 files changed
+1405457
No files matched your search
@@ -0,0 +1,157 @@
|
||||
// S2C 回包二次加密:**等长流密码**(只加密 body,长度、头、checksum 布局全不变)。
|
||||
//
|
||||
// 设计见 `服务端回包二次加密_客户端DLL解密_设计文档_20260930.md`。要点:
|
||||
//
|
||||
// - 加密边界是**协议消息**,不是 TCP 写边界:合批(upperWriteBatch)与 Nagle 行为不受影响;
|
||||
// - **等长**:客户端 `ProxyCipherDecodeFunction` 的契约是 `*outLength = bodyLength`,
|
||||
// 带 tag 的 AEAD 会破坏它,所以这里不用 GCM;
|
||||
// - **无跨包状态**:每包从 `(连接 nonce, msgID, seq)` 重新起流,丢包/重发/跳号都不会失步
|
||||
// (有状态密钥流一旦错位就是"全部乱码 + 客户端卡住",是必须避免的故障形状);
|
||||
// - 密钥按连接派生:HKDF-SHA256(ikm = 固化主密钥, salt = LANA 握手里那 16 字节随机 nonce),
|
||||
// 因此**不需要任何额外往返**,也天然做到"每连接一把"。
|
||||
//
|
||||
// 计数器块(16 字节,AES-CTR 的初始计数块):
|
||||
//
|
||||
// [0:8] 连接 nonce 前 8 字节(每连接随机,来自 LANA challenge)
|
||||
// [8:10] msgID(大端)—— 明文可读,用于降低"同 seq 复用密钥流"的概率
|
||||
// [10:12] seq(大端)—— 明文可读
|
||||
// [12:16] 0,包内块计数器从这里自增
|
||||
//
|
||||
// ⚠️ 已知边界:同一个连接里,`(msgID, seq)` 会在 65536 个包之后重复,
|
||||
// 那时两包会用到同一段密钥流(CTR 复用)。这是为了"绝不失步"付出的代价:
|
||||
// 加包序号纪元(epoch)能消除它,但服务端在写失败时也会 `upperSeq++`,
|
||||
// 客户端观测不到那次自增 → 纪元错位 → **全部乱码**,比理论上的密钥流复用危险得多。
|
||||
// 若要消除,必须先有一个两端都能可靠对齐的通道(协议字段或可靠的连接内信令)。
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/hkdf"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/hex"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
// S2CMasterKeySize 是固化主密钥长度(HKDF 的 ikm)。
|
||||
S2CMasterKeySize = 32
|
||||
// S2CCipherKeySize 是每连接派生出的 AES 密钥长度。
|
||||
S2CCipherKeySize = 16
|
||||
// S2CCipherCodecName 是回包 body 的 codec 标签,写进 game-upper-send-meta 便于排障。
|
||||
S2CCipherCodecName = "s2c-ctr-v1"
|
||||
// s2cKeyInfo 是 HKDF 的域分隔串。
|
||||
s2cKeyInfo = "dnf:s2c:ctr:v1"
|
||||
|
||||
// DefaultS2CMasterKeyBase64 是**固化主密钥**(32 字节 base64)。
|
||||
//
|
||||
// 同一份常量必须同时存在于服务端与 90CN.dll 的 s2ccrypt.h 里——两边不一致 =
|
||||
// 所有回包变成乱码。轮换步骤见设计文档:换这一行 + 改 DLL 常量 + 重新打包,两侧同时上线。
|
||||
// 服务端也认 DNFBRIDGE_GAME_S2C_KEY_BASE64 覆盖(仅供本地联调/灰度)。
|
||||
DefaultS2CMasterKeyBase64 = "ToXJkeLbeccbEN0a2fK2DjRhct6nx8KTYisGp49Levs="
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrS2CMasterKeyInvalid 表示主密钥长度或编码不合法。
|
||||
ErrS2CMasterKeyInvalid = errors.New("dnf protocol: s2c master key is invalid")
|
||||
// ErrS2CNonceInvalid 表示连接 nonce 长度不合法。
|
||||
ErrS2CNonceInvalid = errors.New("dnf protocol: s2c connection nonce is invalid")
|
||||
)
|
||||
|
||||
// S2CCipher 是一条游戏连接上的 S2C body 加解密器(加密与解密是同一个变换)。
|
||||
//
|
||||
// 它是**无状态**的:Apply 只依赖入参与连接级的 block nonce,可以被并发调用。
|
||||
type S2CCipher struct {
|
||||
block cipher.Block
|
||||
// nonce 是连接 nonce 的前 8 字节,来自 LANA challenge 的随机数。
|
||||
nonce [8]byte
|
||||
}
|
||||
|
||||
// ParseS2CMasterKey 解析 32 字节主密钥(base64 / hex / 原始字节)。
|
||||
func ParseS2CMasterKey(data []byte) ([]byte, error) {
|
||||
if len(data) == S2CMasterKeySize {
|
||||
return append([]byte(nil), data...), nil
|
||||
}
|
||||
text := strings.TrimSpace(string(data))
|
||||
if decoded, err := hex.DecodeString(text); err == nil && len(decoded) == S2CMasterKeySize {
|
||||
return decoded, nil
|
||||
}
|
||||
for _, encoding := range []*base64.Encoding{base64.StdEncoding, base64.RawStdEncoding, base64.URLEncoding, base64.RawURLEncoding} {
|
||||
if decoded, err := encoding.DecodeString(text); err == nil && len(decoded) == S2CMasterKeySize {
|
||||
return decoded, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("%w: want %d bytes, got %d input bytes", ErrS2CMasterKeyInvalid, S2CMasterKeySize, len(data))
|
||||
}
|
||||
|
||||
// DefaultS2CMasterKey 返回固化主密钥。
|
||||
func DefaultS2CMasterKey() []byte {
|
||||
key, err := ParseS2CMasterKey([]byte(DefaultS2CMasterKeyBase64))
|
||||
if err != nil {
|
||||
// 常量被改坏时这是构建期就能发现的错误;运行期只可能出现在有人手改常量之后。
|
||||
panic("dnf protocol: baked S2C master key is invalid: " + err.Error())
|
||||
}
|
||||
return key
|
||||
}
|
||||
|
||||
// NewS2CCipher 用主密钥与连接 nonce 派生一条连接的 S2C 加解密器。
|
||||
//
|
||||
// connectionNonce 必须**每连接不同**(服务端用 LANA challenge 里那 16 字节随机数),
|
||||
// 否则不同连接会共用密钥流。
|
||||
func NewS2CCipher(masterKey, connectionNonce []byte) (*S2CCipher, error) {
|
||||
parsed, err := ParseS2CMasterKey(masterKey)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(connectionNonce) != 16 {
|
||||
return nil, fmt.Errorf("%w: want 16 bytes, got %d", ErrS2CNonceInvalid, len(connectionNonce))
|
||||
}
|
||||
salt := append([]byte(nil), connectionNonce...)
|
||||
// HKDF-Extract/Expand;salt 取连接 nonce,info 做域分隔。
|
||||
derived, err := hkdf.Key(sha256.New, parsed, salt, s2cKeyInfo, S2CCipherKeySize)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dnf protocol: derive s2c key: %w", err)
|
||||
}
|
||||
block, err := aes.NewCipher(derived)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("dnf protocol: new s2c cipher: %w", err)
|
||||
}
|
||||
cipher := &S2CCipher{block: block}
|
||||
copy(cipher.nonce[:], connectionNonce[:8])
|
||||
return cipher, nil
|
||||
}
|
||||
|
||||
// Apply 就地变换 body(加密与解密用同一函数)。长度不变。
|
||||
//
|
||||
// msgID 与 seq 必须取**线上 13 字节头里**的值:客户端在两个端点都读得到它们,
|
||||
// 因此不需要任何额外字段来对齐密钥流。
|
||||
func (c *S2CCipher) Apply(body []byte, msgID uint16, seq uint16) {
|
||||
if c == nil || c.block == nil || len(body) == 0 {
|
||||
return
|
||||
}
|
||||
block := c.counterBlock(msgID, seq)
|
||||
stream := cipher.NewCTR(c.block, block[:])
|
||||
stream.XORKeyStream(body, body)
|
||||
}
|
||||
|
||||
// counterBlock 组装该包的 AES-CTR 初始计数块。
|
||||
func (c *S2CCipher) counterBlock(msgID uint16, seq uint16) [16]byte {
|
||||
var block [16]byte
|
||||
copy(block[0:8], c.nonce[:])
|
||||
block[8] = byte(msgID >> 8)
|
||||
block[9] = byte(msgID)
|
||||
block[10] = byte(seq >> 8)
|
||||
block[11] = byte(seq)
|
||||
return block
|
||||
}
|
||||
|
||||
// NoncePrefix 返回连接 nonce 前 8 字节(诊断用;客户端用同一段拼计数器块)。
|
||||
func (c *S2CCipher) NoncePrefix() []byte {
|
||||
if c == nil {
|
||||
return nil
|
||||
}
|
||||
return append([]byte(nil), c.nonce[:]...)
|
||||
}
|
||||
Reference in new issue
Block a user