Files
bd2/go/internal/cryptox/cryptox.go
T
2026-09-21 09:53:46 +08:00

137 lines
4.2 KiB
Go

// Package cryptox implements the small AES envelope used by the BD2 game API.
//
// The protocol key is deliberately represented as its ASCII bytes: a session
// key such as "7eeb...0b15" is not hex-decoded before it is supplied to AES.
package cryptox
import (
"bytes"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"errors"
"fmt"
)
const (
// FixedLoginKey encrypts LoginUser and JoinUser before a user_key exists.
FixedLoginKey = "abcdefghijkrstuv024680wxyzlmnopq"
// AESKeySize is the wire protocol key size (AES-256).
AESKeySize = 32
)
var zeroIV [aes.BlockSize]byte
var (
ErrInvalidKey = errors.New("bd2 crypto: key must be exactly 32 ASCII bytes")
ErrInvalidCiphertext = errors.New("bd2 crypto: ciphertext must be a non-empty AES block sequence")
ErrInvalidPadding = errors.New("bd2 crypto: invalid PKCS#7 padding")
)
// Key returns the fixed pre-login AES key.
func Key() []byte { return []byte(FixedLoginKey) }
// SessionKey validates a login user_key. The protocol passes its 32 ASCII
// characters directly to AES; it does not decode the hexadecimal spelling.
func SessionKey(userKey string) ([]byte, error) {
if len(userKey) != AESKeySize {
return nil, fmt.Errorf("%w: got %d bytes", ErrInvalidKey, len(userKey))
}
for _, c := range []byte(userKey) {
if !(c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F') {
return nil, fmt.Errorf("%w: session key is not hexadecimal ASCII", ErrInvalidKey)
}
}
return []byte(userKey), nil
}
// Encrypt applies PKCS#7 and AES-256-CBC using the all-zero protocol IV.
func Encrypt(plain, key []byte) ([]byte, error) {
if len(key) != AESKeySize {
return nil, ErrInvalidKey
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
padded := pad(plain, block.BlockSize())
out := make([]byte, len(padded))
cipher.NewCBCEncrypter(block, zeroIV[:]).CryptBlocks(out, padded)
return out, nil
}
// Decrypt removes AES-256-CBC and requires valid PKCS#7 padding.
func Decrypt(ciphertext, key []byte) ([]byte, error) {
if len(key) != AESKeySize {
return nil, ErrInvalidKey
}
if len(ciphertext) == 0 || len(ciphertext)%aes.BlockSize != 0 {
return nil, ErrInvalidCiphertext
}
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
out := make([]byte, len(ciphertext))
cipher.NewCBCDecrypter(block, zeroIV[:]).CryptBlocks(out, ciphertext)
return unpad(out, block.BlockSize())
}
// EncryptBase64 returns the API's AES transport value.
func EncryptBase64(plain, key []byte) (string, error) {
ciphertext, err := Encrypt(plain, key)
if err != nil {
return "", err
}
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// DecryptBase64 accepts the API's base64(AES-CBC(...)) transport value.
func DecryptBase64(encoded string, key []byte) ([]byte, error) {
ciphertext, err := base64.StdEncoding.DecodeString(encoded)
if err != nil {
return nil, fmt.Errorf("bd2 crypto: invalid base64 ciphertext: %w", err)
}
return Decrypt(ciphertext, key)
}
// EncryptBase64Payload is the game-message envelope transform. A protobuf
// payload is first base64 encoded, then AES-CBC encrypted, then base64 encoded
// for JSON transport.
func EncryptBase64Payload(payload, key []byte) (string, error) {
return EncryptBase64([]byte(base64.StdEncoding.EncodeToString(payload)), key)
}
// DecryptBase64Payload reverses EncryptBase64Payload and returns protobuf
// bytes directly.
func DecryptBase64Payload(encoded string, key []byte) ([]byte, error) {
inner, err := DecryptBase64(encoded, key)
if err != nil {
return nil, err
}
payload, err := base64.StdEncoding.DecodeString(string(inner))
if err != nil {
return nil, fmt.Errorf("bd2 crypto: invalid inner base64 payload: %w", err)
}
return payload, nil
}
func pad(src []byte, blockSize int) []byte {
n := blockSize - len(src)%blockSize
out := make([]byte, len(src)+n)
copy(out, src)
copy(out[len(src):], bytes.Repeat([]byte{byte(n)}, n))
return out
}
func unpad(src []byte, blockSize int) ([]byte, error) {
if len(src) == 0 || len(src)%blockSize != 0 {
return nil, ErrInvalidPadding
}
n := int(src[len(src)-1])
if n == 0 || n > blockSize || n > len(src) || !bytes.Equal(src[len(src)-n:], bytes.Repeat([]byte{byte(n)}, n)) {
return nil, ErrInvalidPadding
}
return src[:len(src)-n], nil
}