rs-crypto — Cryptographic Primitives for RS-Server
Provides the two cryptographic systems used by the RuneScape protocol:
- RSA — Asymmetric encryption for login block protection (PKCS#8)
- ISAAC — Stream cipher for real-time packet opcode encryption/decryption
Table of Contents
Architecture Overview
┌─────────────────────────────────┐
│ Game Client │
└────────────┬────────────────────┘
│
TCP/WebSocket Connection
│
┌────────────────┼────────────────────┐
│ │ │
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────────┐
│ RSA Decrypt │ │ ISAAC Decode │ │ ISAAC Encode │
│ (login only)│ │ (all packets)│ │ (all packets) │
└──────┬───────┘ └──────┬───────┘ └────────┬─────────┘
│ │ │
▼ ▼ ▼
┌──────────────────────────────────────────────────┐
│ rs-server │
│ │
│ Login Phase: │
│ Client sends RSA-encrypted login block │
│ Server decrypts with private key │
│ Extracts ISAAC seeds from decrypted block │
│ Creates IsaacPair (encode + decode) │
│ │
│ Game Phase: │
│ Every packet opcode XOR'd with ISAAC output │
│ Encode cipher for server → client │
│ Decode cipher for client → server │
└──────────────────────────────────────────────────┘
Login Handshake Flow
Client Server
│ │
│◄──────────── 8-byte ISAAC seed ───────────────│
│ │
│ RSA-encrypt: │
│ magic byte (10) │
│ 4 x ISAAC seeds (i32) │
│ UID │
│ username (base37) │
│ password │
│ │
│──────────── RSA-encrypted block ─────────────►│
│ │
│ RSA decrypt with │
│ private key │
│ Extract seeds │
│ Create IsaacPair │
│ │
│◄──────────── Login response ──────────────────│
│ │
│ ═══════ All subsequent packets ═══════ │
│ Opcodes XOR'd with ISAAC stream │
│◄─────────────────────────────────────────────►│
RSA
Key Format (PKCS#8)
The crate parses PKCS#8 DER-encoded private keys wrapped in PEM:
-----BEGIN PRIVATE KEY-----
MIIEvgIBADANBgkqhkiG9w0BAQEFAASC...
-----END PRIVATE KEY-----
The ASN.1 structure:
SEQUENCE { // PKCS#8 outer wrapper
INTEGER version // 0
SEQUENCE { ... } // AlgorithmIdentifier (RSA)
OCTET STRING { // Private key data
SEQUENCE { // RSA private key
INTEGER version // 0
INTEGER n // modulus
INTEGER e // public exponent
INTEGER d // private exponent
INTEGER p // prime factor 1
INTEGER q // prime factor 2
INTEGER dp // d mod (p-1)
INTEGER dq // d mod (q-1)
INTEGER qinv // q^-1 mod p
}
}
}
RsaKey Struct
All fields use num_bigint::BigInt for arbitrary-precision arithmetic.
Key Loading
// Load from file
let key: RsaKey = load_rsa_key ?;
// Or parse from string
let key: RsaKey = parse_rsa_key_from_pem ?;
Process:
- Strip PEM headers/footers and whitespace
- Base64-decode to DER bytes
- Parse outer PKCS#8 SEQUENCE
- Extract inner OCTET STRING
- Parse RSA private key SEQUENCE
- Extract all 8 integer components as big-endian
BigInt
Usage in Login Handshake
The server loads the RSA key at startup and uses it to decrypt the client's login block. The login block contains ISAAC
seeds, username, and password. The rs-io crate's Packet::rsadec() method performs the actual modular exponentiation
using the key's d and n fields.
ISAAC
Algorithm Overview
ISAAC (Indirection, Shift, Accumulate, Add, and Count) is a cryptographically secure pseudorandom number generator designed by Bob Jenkins. Properties:
| Property | Value |
|---|---|
| Output | 32-bit integers |
| State size | 1 KB (256 x 32-bit words) |
| Batch size | 256 values per generation cycle |
| Speed | ~3 CPU cycles per output byte |
| Seed size | Up to 256 x 32-bit words (this crate uses 4) |
The algorithm maintains internal state (randmem) and produces output in batches of 256 values (randrsl). Each
generation cycle applies indirection, shifting, and accumulation across the full state, making the output stream
unpredictable without knowing the seed.
Rust API
Isaac
// Create from 4 x u32 seed
let mut cipher = new;
// Get next random value
let value: u32 = cipher.next_int;
// Random in range [0, max)
let value: u32 = cipher.next_int_max;
// Random in range [min, max)
let value: i32 = cipher.next_int_range;
IsaacPair
Paired encode/decode ciphers for bidirectional packet encryption:
// From client seeds (standard RuneScape protocol)
// Decode seed = [s0, s1, s2, s3]
// Encode seed = [s0+50, s1+50, s2+50, s3+50]
let pair = from_client_seeds;
// From session key (alternative derivation)
// Decode: [lo, hi, lo+1, hi+1]
// Encode: [lo^0xDEADBEEF, hi^0xCAFEBABE, lo-1, hi-1]
let = from_session_key;
let pair = new;
// Use in packet processing
let opcode_mask = pair.decode.next_int; // decrypt incoming
let opcode_mask = pair.encode.next_int; // encrypt outgoing
Seed Derivation
Two methods for creating encode/decode cipher pairs from shared secrets:
from_client_seeds (Standard Protocol)
Client seeds: [s0, s1, s2, s3] (from RSA-encrypted login block)
Decode seed: [s0, s1, s2, s3 ] (exact client seeds)
Encode seed: [s0+50, s1+50, s2+50, s3+50] (offset by 50)
The +50 offset ensures encode and decode streams never produce identical sequences.
from_session_key (Alternative)
Session key: u64
key_low = session_key & 0xFFFFFFFF
key_high = session_key >> 32
Decode seed: [key_low, key_high,
key_low + 1, key_high + 1 ]
Encode seed: [key_low ^ 0xDEADBEEF, key_high ^ 0xCAFEBABE,
key_low - 1, key_high - 1 ]
C Implementation
The core ISAAC algorithm is implemented in C (csrc/rand.c) for performance, compiled at optimization level 3, and
linked via FFI.
Core Generation (isaac())
For each generation cycle (produces 256 values):
b += ++c // increment counter, feed into b
For each of 256 state positions:
x = mem[i] // load state
a = f(a, i) + mem[i+128 mod 256] // mix accumulator
mem[i] = y = ind(mem, x) + a + b // update state via indirection
rsl[i] = b = ind(mem, y>>8) + x // produce output via indirection
The mixing function f(a, i) cycles through four operations:
| i mod 4 | Operation |
|---|---|
| 0 | a ^= (a << 13) |
| 1 | a ^= (a >> 6) |
| 2 | a ^= (a << 2) |
| 3 | a ^= (a >> 16) |
Initialization (randinit())
1. Set a,b,c,d,e,f,g,h = 0x9E3779B9 (golden ratio)
2. Scramble: 4 iterations of mix(a,b,c,d,e,f,g,h)
3. First pass: mix seed values into state memory
4. Second pass: mix state memory into itself
5. Generate first batch of 256 output values
The golden ratio constant 0x9E3779B9 provides initial entropy dispersion.
Usage in Packet Encryption
Packet Structure (on wire):
┌──────────────────┬─────────────┐
│ opcode (1 byte) │ payload │
│ XOR'd with ISAAC │ (plaintext) │
└──────────────────┴─────────────┘
Encoding (server → client):
wire_opcode = real_opcode ^ (isaac_encode.next_int() & 0xFF)
Decoding (client → server):
real_opcode = wire_opcode ^ (isaac_decode.next_int() & 0xFF)
Both sides maintain synchronized ISAAC streams. Since ISAAC is deterministic with the same seed, and both sides derive seeds from the same handshake data, the streams stay in lockstep as long as both sides consume one value per packet.
Build System
// build.rs
The C code is compiled once at build time into a static library. The Rust FFI layer then calls into randinit() and
isaac() with zero runtime overhead beyond the function call.
File Reference
rs-crypto/
Cargo.toml # deps: thiserror, simple_asn1, base64, num-bigint
build.rs # compiles csrc/rand.c via cc crate
src/
lib.rs # pub mod isaac; pub mod rsa;
rsa.rs # RsaKey, PEM/PKCS#8 parsing, key loading
isaac.rs # Isaac, IsaacPair, FFI bindings, seed derivation
csrc/
rand.c # ISAAC algorithm (Bob Jenkins, public domain)
rand.h # RandCtx struct, RANDSIZ constant
standard.h # ub4/ub1/word type definitions
Dependencies
| Crate | Version | Purpose |
|---|---|---|
thiserror |
2.0 | Error type derivation |
simple_asn1 |
0.6 | ASN.1/DER parsing for PKCS#8 |
base64 |
0.22 | PEM Base64 decoding |
num-bigint |
0.4 | Big integer arithmetic for RSA |
cc |
1.2 | C compiler integration (build-time) |