Files
90CNServer/server/internal/platform/logcrypt/record.go
T
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

207 lines
7.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package logcrypt
import (
"crypto/cipher"
"encoding/binary"
"errors"
"fmt"
"io"
)
// 文件格式(v2:公钥信封)
//
// header := "LHLOG2" | version(1) | keyIDLen(1) | keyID(keyIDLen) | "\n"
// | ephemeralPub(32) | wrapNonce(12) | wrappedLen(u16 LE) | wrappedDEK(wrappedLen)
// record := plainLen(u32 LE) | nonce(12) | AES-256-GCM_DEK(plaintext) + tag(16)
//
// 设计要点:
//
// - 每个文件一把随机 DEK(数据密钥),逐条记录用它做 AES-256-GCM;DEK 用**收件人公钥**
// 封装后写在文件头里。服务端因此只需要公钥,**拿到服务端包也解不开任何日志**。
// - 逐条独立密封:文件被截断时只有最后一条未写完的记录解不开,前面的全部完好;
// 也不需要"整文件才能读"。
// - keyID 写在文件头里:换过密钥对之后,旧文件依然能被对应的旧私钥解开。
// - 记录用整个文件头做 AAD,使记录与"它属于哪个文件、哪把 DEK"绑定,
// 避免记录被跨文件搬运拼接。
const (
fileMagic = "LHLOG2"
fileVersion = 2
// recordOverhead 是每条记录在明文之外多占的字节:长度前缀 + nonce + GCM tag。
recordOverhead = 4 + NonceSize + 16
// headerFixedSize 是文件头里除 keyID 与 wrappedDEK 之外的固定部分。
headerFixedSize = len(fileMagic) + 1 + 1 + 1 + PublicKeySize + NonceSize + 2
)
// ErrRecordTruncated 表示遇到未写完/损坏的记录;之前的记录仍然有效。
var ErrRecordTruncated = errors.New("logcrypt: trailing record is truncated or corrupted")
// headerLayout 是解析出来的文件头。
type headerLayout struct {
// dataOffset 是记录区起始偏移。
dataOffset int
// keyID 是收件人公钥标识。
keyID string
// ephemeralPublic / wrapNonce / wrappedDEK 用于解开文件级 DEK。
ephemeralPublic []byte
wrapNonce []byte
wrappedDEK []byte
}
func buildHeader(recipient *Recipient, ephemeralPublic, wrapNonce, wrappedDEK []byte) []byte {
out := make([]byte, 0, headerFixedSize+len(recipient.keyID)+len(wrappedDEK))
out = append(out, fileMagic...)
out = append(out, fileVersion)
out = append(out, byte(len(recipient.keyID)))
out = append(out, recipient.keyID...)
out = append(out, '\n')
out = append(out, ephemeralPublic...)
out = append(out, wrapNonce...)
var length [2]byte
binary.LittleEndian.PutUint16(length[:], uint16(len(wrappedDEK)))
out = append(out, length[:]...)
out = append(out, wrappedDEK...)
return out
}
func parseHeader(data []byte) (headerLayout, error) {
var layout headerLayout
base := len(fileMagic)
if len(data) < headerFixedSize || string(data[:base]) != fileMagic {
return layout, ErrNotEncrypted
}
if data[base] != fileVersion {
return layout, fmt.Errorf("logcrypt: unsupported container version %d", data[base])
}
idLen := int(data[base+1])
offset := base + 2
if idLen <= 0 || idLen > 64 || len(data) < offset+idLen+1 {
return layout, ErrNotEncrypted
}
layout.keyID = string(data[offset : offset+idLen])
offset += idLen
if data[offset] != '\n' {
return layout, ErrNotEncrypted
}
offset++
if len(data) < offset+PublicKeySize+NonceSize+2 {
return layout, ErrNotEncrypted
}
layout.ephemeralPublic = append([]byte(nil), data[offset:offset+PublicKeySize]...)
offset += PublicKeySize
layout.wrapNonce = append([]byte(nil), data[offset:offset+NonceSize]...)
offset += NonceSize
wrappedLen := int(binary.LittleEndian.Uint16(data[offset : offset+2]))
offset += 2
if wrappedLen <= 0 || len(data) < offset+wrappedLen {
return layout, ErrNotEncrypted
}
layout.wrappedDEK = append([]byte(nil), data[offset:offset+wrappedLen]...)
offset += wrappedLen
layout.dataOffset = offset
return layout, nil
}
// appendRecord 把一条明文记录密封后追加到 dst。
func appendRecord(dst []byte, aead cipher.AEAD, aad []byte, plain []byte, random io.Reader) ([]byte, error) {
if int64(len(plain)) > int64(^uint32(0)) {
return nil, fmt.Errorf("logcrypt: record too large: %d bytes", len(plain))
}
if random == nil {
return nil, errors.New("logcrypt: random source is nil")
}
var nonce [NonceSize]byte
if _, err := io.ReadFull(random, nonce[:]); err != nil {
return nil, fmt.Errorf("logcrypt: nonce: %w", err)
}
var size [4]byte
binary.LittleEndian.PutUint32(size[:], uint32(len(plain)))
dst = append(dst, size[:]...)
dst = append(dst, nonce[:]...)
return aead.Seal(dst, nonce[:], plain, aad), nil
}
// openDEK 用私钥解开文件头里的 DEK,并返回以该 DEK 构造的 AEAD 与记录区起点。
func openDEK(privateKey, data []byte) (cipher.AEAD, headerLayout, error) {
layout, err := parseHeader(data)
if err != nil {
return nil, layout, err
}
dek, err := unwrapDEK(privateKey, layout.ephemeralPublic, layout.wrapNonce, layout.wrappedDEK)
if err != nil {
return nil, layout, err
}
aead, err := newAEAD(dek)
if err != nil {
return nil, layout, err
}
return aead, layout, nil
}
// DecryptAll 用私钥解开一整份容器内容。
//
// 返回值:
// - plaintext:已成功解出的明文(多份记录按写入顺序拼接)
// - records:成功解出的记录条数
// - truncated:为 true 表示末尾有一段未写完/损坏的记录被丢弃(前面的仍然有效)
//
// 只有"文件头不合法"或"首条记录就解不开"这类情况才返回 error——那种情况下
// plaintext 为空,说明私钥不对或者文件不是本格式。
func DecryptAll(privateKey, data []byte) (plaintext []byte, records int, truncated bool, err error) {
if len(privateKey) != PrivateKeySize {
return nil, 0, false, fmt.Errorf("%w: private key must be %d bytes", ErrKeyInvalid, PrivateKeySize)
}
aead, layout, err := openDEK(privateKey, data)
if err != nil {
return nil, 0, false, err
}
offset := layout.dataOffset
aad := data[:offset]
out := make([]byte, 0, len(data))
for offset < len(data) {
if len(data)-offset < recordOverhead {
truncated = true
break
}
plainLen := int(binary.LittleEndian.Uint32(data[offset : offset+4]))
if plainLen < 0 {
truncated = true
break
}
recordLen := recordOverhead + plainLen
if len(data)-offset < recordLen {
truncated = true
break
}
nonce := data[offset+4 : offset+4+NonceSize]
sealed := data[offset+4+NonceSize : offset+recordLen]
plain, openErr := aead.Open(nil, nonce, sealed, aad)
if openErr != nil {
if records == 0 {
return nil, 0, false, fmt.Errorf("logcrypt: cannot decrypt: %w", openErr)
}
truncated = true
break
}
out = append(out, plain...)
records++
offset += recordLen
}
return out, records, truncated, nil
}
// ContainerKeyID 只读文件头,返回该文件使用的收件人公钥标识。用于挑选私钥。
func ContainerKeyID(data []byte) (string, error) {
layout, err := parseHeader(data)
if err != nil {
return "", err
}
return layout.keyID, nil
}
// IsContainer 判断一份字节是否是 logcrypt 容器。回读侧用它区分
// "已加密的记录文件"与"历史遗留的明文文件"。
func IsContainer(data []byte) bool {
_, err := parseHeader(data)
return err == nil
}