mirror of
https://github.com/horizon-rust/horizon.git
synced 2026-11-09 02:29:50 -10:00
Merge origin/main into feat/6484-control-hello
This commit is contained in:
commit
3f2b714ed6
6 files changed
+309
-122
No files matched your search
Generated
+1
@@ -1607,6 +1607,7 @@ dependencies = [
|
||||
name = "horizon-protocol"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"flate2",
|
||||
"getrandom 0.3.4",
|
||||
"horizon-core",
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
Generated
+1
@@ -2760,6 +2760,7 @@ dependencies = [
|
||||
name = "horizon-protocol"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"aes",
|
||||
"flate2",
|
||||
"getrandom 0.3.4",
|
||||
"horizon-core",
|
||||
|
||||
Reference in new issue
Block a user