Documentation
¶
Overview ¶
Package missingcrypt implements the encryption and decryption scheme used by the game's network protocol, as recovered by reverse engineering the client binary. It supports ten block ciphers, several of which deviate from their respective standards. See the README for a full description of the wire format and per-algorithm deviations.
Index ¶
- Variables
- func BuildEnvelope(algorithmID AlgorithmID, headerParam uint32, seedWord uint32, ...) ([]byte, error)
- func ComputeFooter(authKey []byte, body []byte, seed byte) ([]byte, error)
- func Decrypt(input any, authKey []byte) ([]byte, error)
- func DeriveRequestKey(serverKey string, requestTimestampMS int64, deviceUUID string) []byte
- func Encrypt(input any, algorithmID AlgorithmID, authKey []byte, opts EncryptOptions) ([]byte, error)
- func EncryptInner(algorithmID AlgorithmID, headerParam uint32, derivedKey []byte, ...) ([]byte, error)
- func NewMissingCrypt(serverKey string) *missingCrypt
- func VerifyFooter(blob []byte, authKey []byte) error
- type AlgorithmID
- type AlgorithmSpec
- type DecryptedPayload
- type EncryptOptions
- type Envelope
- type InnerPayload
- type WrappedError
Constants ¶
This section is empty.
Variables ¶
var ( ErrInvalidAuthKeyLength = errors.New("missingcrypt: auth key must be at least 32 bytes") )
var ( ErrEnvelopeTooShort = errors.New("missingcrypt: envelope too short") ErrBadHeaderMagic = errors.New("missingcrypt: invalid outer header magic") )
var ErrInvalidIDEAKeyLength = errors.New("missingcrypt: IDEA requires a 16-byte key")
var ErrUnsupportedInputType = errors.New("missingcrypt: input must be []byte or base64 string")
Functions ¶
func BuildEnvelope ¶
func BuildEnvelope(algorithmID AlgorithmID, headerParam uint32, seedWord uint32, randomWord uint32, innerPayload []byte, authKey []byte, bigEndianHeader bool) ([]byte, error)
BuildEnvelope assembles a complete envelope: header + innerPayload + footer. innerPayload is the output of EncryptInner (4-byte clear prefix + ciphertext).
func ComputeFooter ¶
ComputeFooter produces the 32-byte authentication footer appended to every envelope. It is a non-standard HMAC-SHA256 that differs from RFC 2104 in two ways:
The pad order is reversed: the outer pad (opad, 0x5C) is applied in the first hash and the inner pad (ipad, 0x36) in the second, which is the opposite of standard HMAC.
After hashing, each of the eight 32-bit words in the digest is rotated by seed bits. If the LSB of seed is 0 the rotation is right (ror32); if it is 1 the rotation is left (rol32).
body is everything up to but not including the footer region. seed is blob[0], the low byte of the envelope seed word.
func Decrypt ¶
Decrypt accepts an encrypted outer envelope as either raw bytes or a base64-encoded string and returns the decrypted inner payload bytes.
func DeriveRequestKey ¶
DeriveRequestKey produces the 32-byte auth key used for both the envelope footer MAC and the cipher key. The construction is:
key = hex( MD5( serverKey || requestTimestampMS || deviceUUID ) )
requestTimestampMS is the request timestamp in milliseconds, formatted as a decimal string. The resulting MD5 digest (16 bytes) is hex-encoded to 32 ASCII bytes, which serves as the key material.
The 32-byte output is long enough for Blowfish (which uses all 32 bytes) and for all other ciphers (which use the first 16 bytes).
func Encrypt ¶
func Encrypt(input any, algorithmID AlgorithmID, authKey []byte, opts EncryptOptions) ([]byte, error)
Encrypt accepts plaintext as either raw bytes or a base64-encoded string and returns the encrypted outer envelope bytes.
func EncryptInner ¶
func EncryptInner(algorithmID AlgorithmID, headerParam uint32, derivedKey []byte, plaintext []byte) ([]byte, error)
EncryptInner encrypts plaintext and returns the 4-byte clear prefix (XORed length) followed by the CBC ciphertext.
func NewMissingCrypt ¶
func NewMissingCrypt(serverKey string) *missingCrypt
func VerifyFooter ¶
VerifyFooter recomputes the footer for blob and checks it against the last envelopeFooterSize bytes of blob. The seed byte is taken from blob[0].
Types ¶
type AlgorithmID ¶
type AlgorithmID uint32
AlgorithmID is the opaque 32-bit tag embedded in every envelope header that identifies which block cipher was used to encrypt the inner payload. The values are arbitrary wire identifiers assigned by the client; they carry no inherent structure.
const ( AlgAES128 AlgorithmID = 0x021d4314 AlgBlowfish AlgorithmID = 0x03478caf AlgCamellia AlgorithmID = 0x052e3a67 AlgCAST128 AlgorithmID = 0x048a4dfe AlgIDEA AlgorithmID = 0x0951fad3 AlgMARS AlgorithmID = 0x0a325482 AlgMISTY1 AlgorithmID = 0x0b46b571 AlgSEED AlgorithmID = 0x01e6ac1b AlgSerpent AlgorithmID = 0x07fedca9 AlgTwofish AlgorithmID = 0x08a723ab )
Cipher algorithm IDs as they appear on the wire.
type AlgorithmSpec ¶
type AlgorithmSpec struct {
ID AlgorithmID `json:"id"`
Name string `json:"name"`
BlockSize int `json:"block_size"`
KeyBytes int `json:"key_bytes"`
}
AlgorithmSpec holds the static parameters for a supported cipher. KeyBytes is the number of bytes consumed from the derived auth key to form the cipher key; the auth key is always at least 32 bytes (the MD5 hex digest), so every cipher fits within it.
func LookupAlgorithm ¶
func LookupAlgorithm(id AlgorithmID) (AlgorithmSpec, bool)
LookupAlgorithm returns the spec for id, or (zero, false) if unknown.
func MustAlgorithm ¶
func MustAlgorithm(id AlgorithmID) AlgorithmSpec
MustAlgorithm returns the spec for id, panicking if the id is not recognised. Used in internal paths where an unrecognised id indicates a programming error rather than malformed input.
type DecryptedPayload ¶
type DecryptedPayload struct {
Envelope *Envelope `json:"envelope,omitempty"`
Inner *InnerPayload `json:"inner,omitempty"`
}
DecryptedPayload holds both layers of a successfully decrypted message. Envelope is nil when the input was shorter than a full envelope (i.e. the blob was treated as raw plaintext; see decryptPayload).
type EncryptOptions ¶
type EncryptOptions struct {
// HeaderParam is embedded in the envelope header and also seeds the PRNG
// used to derive the IV. It is typically 0 for client-generated traffic.
HeaderParam uint32 `json:"header_param"`
// SeedWord is the 32-bit seed value encoded in bytes 0,6,12,18 of the
// header. Its low byte also drives footer rotation.
SeedWord uint32 `json:"seed_word"`
// RandomWord is used to obfuscate the algorithm ID and header param fields
// in the envelope header.
RandomWord uint32 `json:"random_word"`
// BigEndianHeader causes the algorithm ID, header param, magic, and version
// words to be byte-swapped before encoding. All server-originated traffic
// sets this flag.
BigEndianHeader bool `json:"big_endian_header"`
}
EncryptOptions controls the optional header fields written into the envelope. Zero values are safe: SeedWord and RandomWord are replaced with cryptographically random values when they are 0; HeaderParam defaults to 0, which is valid and selects xor128 for most algorithm IDs.
type Envelope ¶
type Envelope struct {
// SeedByte is the low byte of the seed word (blob[0]). It is also used
// directly as the rotation amount in ComputeFooter.
SeedByte byte `json:"seed_byte"`
// HeaderWord is the decoded seed word assembled from bytes 0,6,12,18.
HeaderWord uint32 `json:"header_word"`
BigEndianHeader bool `json:"big_endian_header"`
AlgorithmID AlgorithmID `json:"algorithm_id"`
AlgorithmName string `json:"algorithm_name"`
// HeaderParam seeds the per-message PRNG that derives the IV and length mask.
HeaderParam uint32 `json:"header_param"`
// RandomWord is used to obfuscate the algorithm ID and header param in the
// encoded header. It is recovered from the bitwise-NOT of bytes 8,14,2,20.
RandomWord uint32 `json:"random_word"`
InnerPrefix []byte `json:"inner_prefix"`
InnerCiphertext []byte `json:"inner_ciphertext"`
Raw []byte `json:"raw"`
}
Envelope holds the parsed fields of a decoded outer envelope. All slice fields are independent copies of the corresponding bytes from the original blob so the caller may safely discard the input after parsing.
func ParseEnvelope ¶
ParseEnvelope decodes the 24-byte interleaved header from blob and returns the envelope fields. The header byte layout is:
byte 0, 6,12,18 → seed word (assembly: little-endian uint32) byte 1, 7,13,19 → version masked = rol32(version XOR seed, seed) byte 2, 8,14,20 → random word, NOT'd (byte order: 16,0,8,24 of randomWord) byte 3, 9,15,21 → algorithm ID masked = algorithmID XOR randomWord byte 4,10,16,22 → header param masked = headerParam XOR bswap32(randomWord) byte 5,11,17,23 → magic masked = ror32(magic XOR seed, seed)
When bigEndianHeader is set the algorithm ID, header param, magic, and version words are byte-swapped before the masking operations above, so the recovered values must be byte-swapped again on decode.
type InnerPayload ¶
type InnerPayload struct {
Plaintext []byte `json:"plaintext"`
IV []byte `json:"iv"`
// LengthXor is the raw PRNG word XORed with the big-endian clear prefix to
// recover the true plaintext length.
LengthXor uint32 `json:"length_xor"`
// PRNGKind is "mt19937" or "xor128", indicating which generator was used
// to derive the IV and length mask for this message.
PRNGKind string `json:"prng_kind"`
}
InnerPayload holds the decrypted inner content together with the metadata needed to reproduce or verify the encryption.
func DecryptInner ¶
func DecryptInner(algorithmID AlgorithmID, headerParam uint32, derivedKey []byte, clearPrefix []byte, ciphertext []byte) (*InnerPayload, error)
DecryptInner decrypts the inner CBC payload and returns the plaintext together with the metadata needed to reproduce the encryption. clearPrefix is the 4-byte big-endian word immediately following the envelope header; it holds the plaintext length XORed with a PRNG-derived mask.
type WrappedError ¶
WrappedError pairs a human-readable message with an underlying error and an optional map of structured context values for programmatic inspection. ctx is unexported so it is only accessible within the package.
func (WrappedError) Error ¶
func (we WrappedError) Error() string