Files
hanfuit 645b64fe21 DNF90 服务端与启动器完整源码(含 GM 工具功能增强)
服务端(Go):
- GM 塞背包/带属性装备发放:在线角色推送 op14 增量物品行,背包实时刷新,无需回角色选择界面
- GM 改金币/改等级:去掉在线拒绝,落库后推送 op14 钱包/op37 等级状态,在线实时刷新
- 等级端点统一切换到新版 GMSetCharacterLevel(自带在线刷新)
- 无限/锁死疲劳模式 + 燃烧疲劳:每房间消耗 2 点、打怪与通关经验双倍
- 燃烧疲劳开关时给所有在线角色推送系统提示(op12 mode 0)
- 深渊史诗掉率:GM 全局倍率 + 城镇增益药剂窗口倍率,真实接入掉落结算

启动器(Python/JS):
- 标题栏自绘、窗口 1204x717、禁止拖拽改大小、仅最小化/关闭
- GM 工具:疲劳与燃烧、深渊史诗掉率、拍卖行上架(单一定价对齐)
- 拍卖行上架前端对齐服务端单一价格语义,修复 400 报错
2026-11-04 14:31:55 +08:00

342 lines
9.2 KiB
Go

package protocol
import (
"crypto/aes"
"crypto/cipher"
"crypto/hkdf"
"crypto/rand"
"crypto/sha256"
"encoding/binary"
"errors"
"fmt"
"io"
"sync"
)
// frame 加密参数使用 AES-GCM 推荐的 32 字节 key 和 12 字节 nonce。
const (
FrameSessionKeySize = 32
FrameNonceSize = 12
)
// frame 加密和重放保护错误。
var (
ErrFrameEncryptionKeyInvalid = errors.New("protocol frame encryption key is invalid")
ErrFrameEncryptedPayload = errors.New("protocol frame encrypted payload is invalid")
ErrFrameNonceReplay = errors.New("protocol frame nonce replay detected")
ErrFrameReplayGuardFull = errors.New("protocol frame replay guard is full")
)
// DeriveFrameSessionKey 从主密钥和会话 ID 派生 frame 会话密钥。
func DeriveFrameSessionKey(masterSecret, sessionID string) ([]byte, error) {
if masterSecret == "" || sessionID == "" {
return nil, ErrFrameEncryptionKeyInvalid
}
key, err := hkdf.Key(sha256.New, []byte(masterSecret), []byte(sessionID), "longheng:frame-session-key:v1", FrameSessionKeySize)
if err != nil {
return nil, err
}
return key, nil
}
// EncryptFrame 加密 frame body,并把 nonce 放入密文前缀。
func EncryptFrame(frame Frame, key []byte, random io.Reader) (Frame, error) {
if frame.Version == 0 {
// 加密 AAD 必须与最终写出的帧头一致;WriteFrame 会把 0 规范成 FrameVersion1。
frame.Version = FrameVersion1
}
if frame.Version != FrameVersion1 {
return Frame{}, fmt.Errorf("%w: %d", ErrInvalidFrameVersion, frame.Version)
}
aead, err := frameAEAD(key)
if err != nil {
return Frame{}, err
}
if random == nil {
random = rand.Reader
}
nonce := make([]byte, FrameNonceSize)
binary.BigEndian.PutUint64(nonce[:8], frame.Sequence)
if _, err := io.ReadFull(random, nonce[8:]); err != nil {
return Frame{}, err
}
out := frame
out.Flags |= FrameFlagEncrypted
aad, err := frameAAD(frame)
if err != nil {
return Frame{}, err
}
out.Body = aead.Seal(nonce, nonce, frame.Body, aad)
out.Checksum = 0
return out, nil
}
// WriteEncryptedFrame 加密后写出 frame_v1。
func WriteEncryptedFrame(w io.Writer, frame Frame, key []byte, random io.Reader) error {
encrypted, err := EncryptFrame(frame, key, random)
if err != nil {
return err
}
return WriteFrame(w, encrypted)
}
// DecryptFrame 解密 frame body 并执行可选 nonce 重放检查。
func DecryptFrame(frame Frame, key []byte, replay *FrameReplayGuard) (Frame, error) {
if frame.Flags&FrameFlagEncrypted == 0 {
return frame, nil
}
aead, err := frameAEAD(key)
if err != nil {
return Frame{}, err
}
if len(frame.Body) < FrameNonceSize+aead.Overhead() {
return Frame{}, ErrFrameEncryptedPayload
}
nonce := append([]byte(nil), frame.Body[:FrameNonceSize]...)
aad, err := frameAAD(frame)
if err != nil {
return Frame{}, err
}
plain, err := aead.Open(nil, nonce, frame.Body[FrameNonceSize:], aad)
if err != nil {
return Frame{}, fmt.Errorf("%w: %w", ErrFrameEncryptedPayload, err)
}
if replay != nil {
if err := replay.Seen(nonce); err != nil {
return Frame{}, err
}
}
out := frame
out.Flags &^= FrameFlagEncrypted
out.Body = plain
out.Checksum = 0
return out, nil
}
// ReadEncryptedFrame 读取 frame_v1 后立即解密。
func ReadEncryptedFrame(r io.Reader, maxBodySize uint32, key []byte, replay *FrameReplayGuard) (Frame, error) {
frame, err := ReadFrame(r, maxBodySize)
if err != nil {
return Frame{}, err
}
return DecryptFrame(frame, key, replay)
}
// EncryptEnvelope 把 envelope 按 frame_v1 规则加密。
func EncryptEnvelope(envelope Envelope, key []byte, random io.Reader) (Envelope, error) {
if NormalizeWireFormat(envelope.Format) != WireFormatFrameV1 {
return Envelope{}, ErrInvalidWireFormat
}
frame := Frame{
Version: envelope.Frame.Version,
Flags: envelope.Frame.Flags,
Codec: envelope.Frame.Codec,
PacketID: envelope.Packet.PacketID,
MsgID: envelope.Packet.MsgID,
Sequence: envelope.Frame.Sequence,
Body: append([]byte(nil), envelope.Packet.Body...),
Metadata: CloneFrameMetadata(envelope.Frame.Metadata),
}
if envelope.Packet.Compressed {
frame.Flags |= FrameFlagCompressed
}
encrypted, err := EncryptFrame(frame, key, random)
if err != nil {
return Envelope{}, err
}
return Envelope{
Format: WireFormatFrameV1,
Packet: packetFromFrame(encrypted),
Frame: encrypted,
}, nil
}
// DecryptEnvelope 解密 frame_v1 envelope,旧 packet envelope 直接透传。
func DecryptEnvelope(envelope Envelope, key []byte, replay *FrameReplayGuard) (Envelope, error) {
switch NormalizeWireFormat(envelope.Format) {
case WireFormatPacket:
return envelope, nil
case WireFormatFrameV1:
default:
return Envelope{}, ErrInvalidWireFormat
}
if envelope.Frame.Flags&FrameFlagEncrypted == 0 {
return envelope, nil
}
decrypted, err := DecryptFrame(envelope.Frame, key, replay)
if err != nil {
return Envelope{}, err
}
return Envelope{
Format: WireFormatFrameV1,
Packet: packetFromFrame(decrypted),
Frame: decrypted,
}, nil
}
// FrameReplayGuard 在有界序号窗口内精确记录 nonce,防止同一会话内加密帧重放。
//
// nonce 前 8 字节是大端 frame sequence,后 4 字节是随机后缀。同一 sequence
// 可能因响应和 push 编号空间重叠而出现不同 nonce,因此窗口内按完整 nonce
// 去重;一旦 sequence 滑出窗口,其任何 nonce 都永久按重放拒绝。
type FrameReplayGuard struct {
mu sync.Mutex
max int
initialized bool
highest uint64
total int
seen map[[FrameNonceSize]byte]struct{}
slots []frameReplaySlot
}
type frameReplaySlot struct {
sequence uint64
active bool
nonces [][FrameNonceSize]byte
}
// NewFrameReplayGuard 创建固定序号窗口和 nonce 容量的重放保护器。
func NewFrameReplayGuard(maxEntries int) *FrameReplayGuard {
if maxEntries <= 0 {
maxEntries = 4096
}
return &FrameReplayGuard{
max: maxEntries,
seen: make(map[[FrameNonceSize]byte]struct{}, maxEntries),
slots: make([]frameReplaySlot, maxEntries),
}
}
// Seen 记录 nonce;重复 nonce 或已滑出窗口的旧序号会返回重放错误。
func (g *FrameReplayGuard) Seen(nonce []byte) error {
if g == nil {
return ErrFrameEncryptedPayload
}
if len(nonce) != FrameNonceSize {
return ErrFrameEncryptedPayload
}
var key [FrameNonceSize]byte
copy(key[:], nonce)
sequence := binary.BigEndian.Uint64(key[:8])
g.mu.Lock()
defer g.mu.Unlock()
g.ensureReady()
window := replayWindow(g.max)
if g.initialized && sequence <= g.highest && g.highest-sequence >= window {
return ErrFrameNonceReplay
}
if !g.initialized {
g.initialized = true
g.highest = sequence
} else if sequence > g.highest {
g.advance(sequence)
}
if _, ok := g.seen[key]; ok {
return ErrFrameNonceReplay
}
if g.total >= g.max {
return ErrFrameReplayGuardFull
}
slot := &g.slots[sequence%window]
if slot.active && slot.sequence != sequence {
g.clearSlot(slot)
}
if !slot.active {
slot.sequence = sequence
slot.active = true
}
slot.nonces = append(slot.nonces, key)
g.seen[key] = struct{}{}
g.total++
return nil
}
func (g *FrameReplayGuard) ensureReady() {
if g.max <= 0 {
g.max = 4096
}
if g.seen == nil {
g.seen = make(map[[FrameNonceSize]byte]struct{}, g.max)
}
if len(g.slots) != g.max {
g.slots = make([]frameReplaySlot, g.max)
g.seen = make(map[[FrameNonceSize]byte]struct{}, g.max)
g.initialized = false
g.highest = 0
g.total = 0
}
}
func (g *FrameReplayGuard) advance(sequence uint64) {
delta := sequence - g.highest
window := replayWindow(g.max)
if delta >= window {
g.slots = make([]frameReplaySlot, g.max)
g.seen = make(map[[FrameNonceSize]byte]struct{}, g.max)
g.total = 0
g.highest = sequence
return
}
start := g.highest % window
for step := uint64(1); step <= delta; step++ {
g.clearSlot(&g.slots[(start+step)%window])
}
g.highest = sequence
}
func replayWindow(maxEntries int) uint64 {
if maxEntries <= 0 {
return 4096
}
// int 的正值范围始终可由 uint64 表示;这里集中转换,避免窗口运算散落窄化风险。
return uint64(maxEntries) // #nosec G115 -- maxEntries 已验证为正数,转换不会溢出。
}
func (g *FrameReplayGuard) clearSlot(slot *frameReplaySlot) {
if slot == nil || !slot.active {
return
}
for _, nonce := range slot.nonces {
delete(g.seen, nonce)
g.total--
}
if g.total < 0 {
g.total = 0
}
*slot = frameReplaySlot{}
}
func frameAEAD(key []byte) (cipher.AEAD, error) {
if len(key) != FrameSessionKeySize {
return nil, ErrFrameEncryptionKeyInvalid
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
return cipher.NewGCM(block)
}
func frameAAD(frame Frame) ([]byte, error) {
var aad [21]byte
aad[0] = frame.Version
binary.BigEndian.PutUint16(aad[1:3], frame.Flags&^FrameFlagEncrypted)
binary.BigEndian.PutUint16(aad[3:5], frame.Codec)
binary.BigEndian.PutUint32(aad[5:9], packetIDToWire(frame.PacketID))
binary.BigEndian.PutUint32(aad[9:13], frame.MsgID)
binary.BigEndian.PutUint64(aad[13:21], frame.Sequence)
metadata, err := encodeFrameMeta(frame.Metadata)
if err != nil {
return nil, err
}
if len(metadata) == 0 {
return aad[:], nil
}
out := make([]byte, 0, len(aad)+len(metadata))
out = append(out, aad[:]...)
out = append(out, metadata...)
return out, nil
}