Merge origin/main into feat/6484-control-hello

This commit is contained in:
Akirabane committed 2026-11-01 23:30:03 +01:00
commit 3f2b714ed6
6 files changed
+309 -122

No files matched your search

Generated
+1
View File
@@ -1607,6 +1607,7 @@ dependencies = [
name = "horizon-protocol"
version = "0.1.0"
dependencies = [
"aes",
"flate2",
"getrandom 0.3.4",
"horizon-core",
+2
View File
@@ -16,6 +16,8 @@ getrandom = "0.3"
sha1 = "0.10"
# Secure chat: SHA256withRSA message signatures.
sha2 = "0.10"
# AES-NI at run time on x86 for the connection cipher (crypto/blocks.rs).
aes = "0.8.4"
# The name servers of Windows' network adapters, for Minecraft SRV redirects.
[target.'cfg(windows)'.dependencies]
+96 -121
View File
@@ -5,107 +5,15 @@
//! needed: CFB8 encrypts the shift register in both directions.
use sha1::{Digest, Sha1};
const SBOX: [u8; 256] = {
// Generated from the AES field inverse and affine transform at compile time.
let mut sbox = [0u8; 256];
let mut p: u8 = 1;
let mut q: u8 = 1;
loop {
// p *= 3
p = p ^ (p << 1) ^ if p & 0x80 != 0 { 0x1B } else { 0 };
// q /= 3
q ^= q << 1;
q ^= q << 2;
q ^= q << 4;
if q & 0x80 != 0 {
q ^= 0x09;
}
let x = q ^ q.rotate_left(1) ^ q.rotate_left(2) ^ q.rotate_left(3) ^ q.rotate_left(4);
sbox[p as usize] = x ^ 0x63;
if p == 1 {
break;
}
}
sbox[0] = 0x63;
sbox
};
fn xtime(b: u8) -> u8 {
(b << 1) ^ if b & 0x80 != 0 { 0x1B } else { 0 }
}
/// AES-128 block encryption (FIPS-197).
#[derive(Clone)]
pub struct Aes128 {
round_keys: [[u8; 16]; 11],
}
impl Aes128 {
pub fn new(key: &[u8; 16]) -> Self {
let mut w = [[0u8; 4]; 44];
for i in 0..4 {
w[i].copy_from_slice(&key[i * 4..i * 4 + 4]);
}
let mut rcon = 1u8;
for i in 4..44 {
let mut t = w[i - 1];
if i % 4 == 0 {
t = [
SBOX[t[1] as usize] ^ rcon,
SBOX[t[2] as usize],
SBOX[t[3] as usize],
SBOX[t[0] as usize],
];
rcon = xtime(rcon);
}
for j in 0..4 {
w[i][j] = w[i - 4][j] ^ t[j];
}
}
let mut round_keys = [[0u8; 16]; 11];
for (r, key) in round_keys.iter_mut().enumerate() {
for c in 0..4 {
key[c * 4..c * 4 + 4].copy_from_slice(&w[r * 4 + c]);
}
}
Self { round_keys }
}
pub fn encrypt_block(&self, block: &mut [u8; 16]) {
let add = |s: &mut [u8; 16], k: &[u8; 16]| s.iter_mut().zip(k).for_each(|(a, b)| *a ^= b);
add(block, &self.round_keys[0]);
for round in 1..=10 {
for b in block.iter_mut() {
*b = SBOX[*b as usize];
}
// ShiftRows (column-major state: byte r + 4c).
let s = *block;
for c in 0..4 {
for r in 0..4 {
block[r + 4 * c] = s[r + 4 * ((c + r) % 4)];
}
}
if round != 10 {
for c in 0..4 {
let col = [
block[4 * c],
block[4 * c + 1],
block[4 * c + 2],
block[4 * c + 3],
];
let all = col[0] ^ col[1] ^ col[2] ^ col[3];
for r in 0..4 {
block[4 * c + r] = col[r] ^ all ^ xtime(col[r] ^ col[(r + 1) % 4]);
}
}
}
add(block, &self.round_keys[round]);
}
}
}
mod blocks;
pub use blocks::Blocks as Aes128;
use blocks::LANES;
/// AES/CFB8 stream state for one direction of a connection.
#[derive(Clone)]
pub struct Cfb8 {
aes: Aes128,
/// The last 16 ciphertext bytes (the IV before any).
register: [u8; 16],
}
impl Cfb8 {
@@ -116,26 +24,28 @@ impl Cfb8 {
register: *iv,
}
}
fn keystream(&self) -> u8 {
let mut block = self.register;
self.aes.encrypt_block(&mut block);
block[0]
}
fn shift(&mut self, cipher_byte: u8) {
self.register.copy_within(1.., 0);
self.register[15] = cipher_byte;
}
/// Each byte's keystream is the encrypted register, which holds the
/// ciphertext before it: encryption goes one byte at a time.
pub fn encrypt(&mut self, data: &mut [u8]) {
for b in data {
*b ^= self.keystream();
self.shift(*b);
}
self.aes.cfb8_encrypt(&mut self.register, data);
}
/// Decryption knows every ciphertext byte up front, so the registers of
/// [`LANES`] bytes in a row are known together and encrypted at once.
pub fn decrypt(&mut self, data: &mut [u8]) {
for b in data {
let c = *b;
*b ^= self.keystream();
self.shift(c);
for run in data.chunks_mut(LANES) {
// The register, then this run's ciphertext: byte i's register
// is window[i..i + 16].
let mut window = [0u8; 16 + LANES];
window[..16].copy_from_slice(&self.register);
window[16..16 + run.len()].copy_from_slice(run);
let mut blocks: [[u8; 16]; LANES] =
std::array::from_fn(|i| window[i..i + 16].try_into().unwrap());
self.aes.encrypt_lanes(&mut blocks);
for (b, k) in run.iter_mut().zip(&blocks) {
*b ^= k[0];
}
self.register
.copy_from_slice(&window[run.len()..run.len() + 16]);
}
}
}
@@ -279,14 +189,6 @@ mod tests {
.collect()
}
#[test]
fn sbox_spot_checks() {
assert_eq!(SBOX[0x00], 0x63);
assert_eq!(SBOX[0x01], 0x7c);
assert_eq!(SBOX[0x53], 0xed);
assert_eq!(SBOX[0xff], 0x16);
}
#[test]
fn aes128_fips197_vector() {
let key: [u8; 16] = hex("000102030405060708090a0b0c0d0e0f").try_into().unwrap();
@@ -313,6 +215,79 @@ mod tests {
assert_eq!(data, plain);
}
/// Bytes from a fixed xorshift: the same every run.
fn noise(n: usize, mut seed: u64) -> Vec<u8> {
(0..n)
.map(|_| {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
seed as u8
})
.collect()
}
#[test]
fn lanes_match_single_blocks_and_the_aes_crate() {
use aes::cipher::{BlockEncrypt, KeyInit};
let key: [u8; 16] = noise(16, 1).try_into().unwrap();
let aes = Aes128::new(&key);
let reference = aes::Aes128::new(&key.into());
let mut lanes: [[u8; 16]; LANES] =
std::array::from_fn(|i| noise(16, 7 + i as u64).try_into().unwrap());
let inputs = lanes;
aes.encrypt_lanes(&mut lanes);
for (input, out) in inputs.iter().zip(&lanes) {
let mut one = *input;
aes.encrypt_block(&mut one);
assert_eq!(&one, out);
let mut expected = aes::Block::from(*input);
reference.encrypt_block(&mut expected);
assert_eq!(out[..], expected[..]);
}
}
/// Decryption in runs of [`LANES`] gives what the one-byte-at-a-time
/// definition gives, however the stream is cut into reads.
#[test]
fn cfb8_decrypts_like_the_byte_by_byte_definition() {
let key: [u8; 16] = noise(16, 3).try_into().unwrap();
let iv: [u8; 16] = noise(16, 4).try_into().unwrap();
let plain = noise(5000, 5);
let mut cipher = plain.clone();
Cfb8::new(&key, &iv).encrypt(&mut cipher);
// The definition: P[i] = C[i] ^ E(register)[0], register += C[i].
let aes = Aes128::new(&key);
let mut register = iv;
let expected: Vec<u8> = cipher
.iter()
.map(|&c| {
let mut block = register;
aes.encrypt_block(&mut block);
register.copy_within(1.., 0);
register[15] = c;
c ^ block[0]
})
.collect();
assert_eq!(expected, plain);
for cuts in [
&[5000][..],
&[1, 7, 8, 9, 15, 16, 17, 1000, 3927],
&[3, 4997],
] {
let mut data = cipher.clone();
let mut dec = Cfb8::new(&key, &iv);
let mut rest = &mut data[..];
for &n in cuts {
let (head, tail) = rest.split_at_mut(n);
dec.decrypt(head);
rest = tail;
}
assert!(rest.is_empty());
assert_eq!(data, plain, "cuts {cuts:?}");
}
}
/// The digests of single names published with the protocol docs.
#[test]
fn server_hash_vectors() {
@@ -0,0 +1,207 @@
//! AES-128 encryption of several blocks at once, for CFB8.
//!
//! aarch64 builds that have the AES extension at compile time (every Apple Silicon Mac:
//! `aarch64-apple-darwin` targets the M1) use the ARMv8 AES instructions here directly.
//! Everywhere else the RustCrypto `aes` crate (MIT OR Apache-2.0) does it: AES-NI chosen at
//! run time on x86 and x86_64, a constant-time software AES on CPUs without it.
//!
//! [`LANES`] blocks go through at once: the AES instructions take a few cycles each but
//! start one per cycle, so independent blocks overlap. CFB8 decryption has [`LANES`]
//! independent blocks for every [`LANES`] bytes (see `Cfb8::decrypt`).
/// Blocks encrypted per call: 8 fill the ARM pipelines; the `aes` crate pays a fixed
/// cost per call, which 32 blocks spread (measured on an AMD EPYC 7662).
pub use imp::LANES;
/// One expanded AES-128 key.
#[derive(Clone)]
pub struct Blocks(imp::Key);
impl Blocks {
pub fn new(key: &[u8; 16]) -> Self {
Self(imp::Key::new(key))
}
/// Encrypt one block in place.
pub fn encrypt_block(&self, block: &mut [u8; 16]) {
self.0.encrypt_block(block);
}
/// CFB8 encryption: each byte's keystream is the encrypted register,
/// which then shifts in that byte's ciphertext.
pub fn cfb8_encrypt(&self, register: &mut [u8; 16], data: &mut [u8]) {
self.0.cfb8_encrypt(register, data);
}
/// Encrypt [`LANES`] blocks in place.
pub fn encrypt_lanes(&self, blocks: &mut [[u8; 16]; LANES]) {
self.0.encrypt_lanes(blocks);
}
}
// The only unsafe code in this crate: the ARMv8 AES intrinsics are `#[target_feature]`
// functions, which Rust lets safe code call only from a function with the same feature.
#[cfg(all(target_arch = "aarch64", target_feature = "aes"))]
#[allow(unsafe_code)]
mod imp {
use core::arch::aarch64::{
uint8x16_t, vaeseq_u8, vaesmcq_u8, vcombine_u8, vcreate_u8, vdupq_n_u8, veorq_u8, vextq_u8,
vgetq_lane_u8, vgetq_lane_u64, vreinterpretq_u64_u8,
};
pub const LANES: usize = 8;
/// FIPS-197's S-box, for the key schedule only (the rounds use AESE).
const SBOX: [u8; 256] = {
let mut sbox = [0u8; 256];
let (mut p, mut q) = (1u8, 1u8);
loop {
// p *= 3, q /= 3 in GF(2^8).
p = p ^ (p << 1) ^ if p & 0x80 != 0 { 0x1B } else { 0 };
q ^= q << 1;
q ^= q << 2;
q ^= q << 4;
if q & 0x80 != 0 {
q ^= 0x09;
}
let x = q ^ q.rotate_left(1) ^ q.rotate_left(2) ^ q.rotate_left(3) ^ q.rotate_left(4);
sbox[p as usize] = x ^ 0x63;
if p == 1 {
break;
}
}
sbox[0] = 0x63;
sbox
};
/// FIPS-197 key expansion: 11 round keys, in the byte order AESE takes.
fn round_keys(key: &[u8; 16]) -> [[u8; 16]; 11] {
let mut w = [[0u8; 4]; 44];
for i in 0..4 {
w[i].copy_from_slice(&key[i * 4..i * 4 + 4]);
}
let mut rcon = 1u8;
for i in 4..44 {
let mut t = w[i - 1];
if i % 4 == 0 {
t = [
SBOX[t[1] as usize] ^ rcon,
SBOX[t[2] as usize],
SBOX[t[3] as usize],
SBOX[t[0] as usize],
];
rcon = (rcon << 1) ^ if rcon & 0x80 != 0 { 0x1B } else { 0 };
}
for j in 0..4 {
w[i][j] = w[i - 4][j] ^ t[j];
}
}
let mut keys = [[0u8; 16]; 11];
for (r, k) in keys.iter_mut().enumerate() {
for c in 0..4 {
k[c * 4..c * 4 + 4].copy_from_slice(&w[r * 4 + c]);
}
}
keys
}
#[derive(Clone, Copy)]
pub struct Key([uint8x16_t; 11]);
impl Key {
pub fn new(key: &[u8; 16]) -> Self {
// SAFETY: as in `encrypt_block`.
Self(round_keys(key).map(|k| unsafe { load(&k) }))
}
pub fn encrypt_block(&self, block: &mut [u8; 16]) {
// SAFETY: this module is compiled only for targets whose CPUs all have
// the AES extension (`cfg(target_feature = "aes")`), and NEON comes with it.
unsafe { encrypt::<1>(&self.0, std::slice::from_mut(block).try_into().unwrap()) }
}
pub fn encrypt_lanes(&self, blocks: &mut [[u8; 16]; LANES]) {
// SAFETY: as in `encrypt_block`.
unsafe { encrypt(&self.0, blocks) }
}
pub fn cfb8_encrypt(&self, register: &mut [u8; 16], data: &mut [u8]) {
// SAFETY: as in `encrypt_block`.
unsafe { cfb8_encrypt(&self.0, register, data) }
}
}
/// The register stays in a vector: one AES per byte and a byte shift.
#[target_feature(enable = "neon,aes")]
fn cfb8_encrypt(k: &[uint8x16_t; 11], register: &mut [u8; 16], data: &mut [u8]) {
let mut r = load(register);
for b in data {
let mut s = r;
for k in &k[..9] {
s = vaesmcq_u8(vaeseq_u8(s, *k));
}
let s = veorq_u8(vaeseq_u8(s, k[9]), k[10]);
*b ^= vgetq_lane_u8::<0>(s);
r = vextq_u8::<1>(r, vdupq_n_u8(*b));
}
store(r, register);
}
#[target_feature(enable = "neon,aes")]
fn store(v: uint8x16_t, b: &mut [u8; 16]) {
let v = vreinterpretq_u64_u8(v);
b[..8].copy_from_slice(&vgetq_lane_u64::<0>(v).to_le_bytes());
b[8..].copy_from_slice(&vgetq_lane_u64::<1>(v).to_le_bytes());
}
#[target_feature(enable = "neon,aes")]
fn load(b: &[u8; 16]) -> uint8x16_t {
let half = |i: usize| u64::from_le_bytes(b[i..i + 8].try_into().unwrap());
vcombine_u8(vcreate_u8(half(0)), vcreate_u8(half(8)))
}
/// AESE is AddRoundKey, SubBytes and ShiftRows; AESMC is MixColumns; the last
/// round has no MixColumns and ends with the eleventh key. Rounds go across all
/// `N` blocks before the next, so the blocks' instructions overlap.
#[target_feature(enable = "neon,aes")]
fn encrypt<const N: usize>(k: &[uint8x16_t; 11], blocks: &mut [[u8; 16]; N]) {
let mut s = blocks.each_ref().map(|b| load(b));
for k in &k[..9] {
for v in &mut s {
*v = vaesmcq_u8(vaeseq_u8(*v, *k));
}
}
for (v, b) in s.into_iter().zip(blocks) {
store(veorq_u8(vaeseq_u8(v, k[9]), k[10]), b);
}
}
}
#[cfg(not(all(target_arch = "aarch64", target_feature = "aes")))]
mod imp {
use aes::cipher::{BlockEncrypt, KeyInit};
pub const LANES: usize = 32;
#[derive(Clone)]
pub struct Key(aes::Aes128);
impl Key {
pub fn new(key: &[u8; 16]) -> Self {
Self(aes::Aes128::new(key.into()))
}
pub fn encrypt_block(&self, block: &mut [u8; 16]) {
self.0.encrypt_block(block.into());
}
pub fn cfb8_encrypt(&self, register: &mut [u8; 16], data: &mut [u8]) {
for b in data {
let mut block = aes::Block::from(*register);
self.0.encrypt_block(&mut block);
*b ^= block[0];
register.copy_within(1.., 0);
register[15] = *b;
}
}
pub fn encrypt_lanes(&self, blocks: &mut [[u8; 16]; LANES]) {
let mut b = blocks.map(aes::Block::from);
self.0.encrypt_blocks(&mut b);
for (out, b) in blocks.iter_mut().zip(b) {
out.copy_from_slice(&b);
}
}
}
}
+2 -1
View File
@@ -1,7 +1,8 @@
//! The Minecraft Java Edition network protocol (of the version in `horizon_core::version`):
//! primitive codecs, framing, compression, encryption, and typed packets for
//! every connection state, shared by the Horizon client and (later) server.
#![forbid(unsafe_code)]
// `deny`, not `forbid`: crypto/blocks.rs allows the ARMv8 AES intrinsics, nothing else.
#![deny(unsafe_code)]
pub mod buf;
pub mod chunk;
pub mod connection;
+1
View File
@@ -2760,6 +2760,7 @@ dependencies = [
name = "horizon-protocol"
version = "0.1.0"
dependencies = [
"aes",
"flate2",
"getrandom 0.3.4",
"horizon-core",