137 lines
4.2 KiB
Go
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
|
|
}
|