Make into optional features: authentication, compression, encryption.

master
James T. Martin 2020-07-26 14:33:51 -07:00
parent 60c9a48293
commit 5302c04fa4
Signed by: james
GPG Key ID: 4B7F3DA9351E577C
5 changed files with 96 additions and 62 deletions

View File

@ -7,6 +7,26 @@ repository = "https://github.com/jamestmartin/tmd"
license = "GPL-3.0+"
publish = false
[features]
default = ["authentication", "compression", "encryption"]
# Enables authentication via Mojang's servers, i.e. online mode.
authentication = ["encryption", "num-bigint", "sha-1", "reqwest"]
# Enables protocol compression.
compression = ["flate2"]
# Enables protocol encryption *without enabling authentication*.
#
# The protocol itself doesn't depend on authentication to use encryption.
# If the client and server both simply *don't* check in with Mojang,
# then the protocol will proceed as normal, but encrypted.
# However, be warned that there is no way to disable authentication
# for Notchian clients (they will disconnect you from the server),
# so a modified client would be necessary
# to use offline encryption, and none exist that I know of.
encryption = ["aes", "cfb8", "rand", "rsa"]
[dependencies]
async-trait = "0.1.36"
clap = { version = "2.33.1", features = ["yaml"] }
@ -15,14 +35,18 @@ serde_json = "1.0.56"
tokio = { version = "0.2.22", features = ["io-util", "macros", "net", "tcp", "rt-threaded"] }
uuid = { version = "0.8", features = ["serde"] }
# Dependencies required for authentication
# Used to format Minecraft's "server id" hash.
num-bigint = { version = "0.3.0", optional = true }
sha-1 = { version = "0.9.1", optional = true }
# Used to make the request to the Mojang servers.
reqwest = { version = "0.10.7", optional = true }
# Dependencies required for compression
flate2 = "1.0.16"
flate2 = { version = "1.0.16", optional = true }
# Dependencies required for encryption
aes = "0.4.0"
cfb8 = "0.4.0"
num-bigint = "0.3.0"
rand = "0.7.3"
reqwest = "0.10.7"
rsa = "0.3.0"
sha-1 = "0.9.1"
aes = { version = "0.4.0", optional = true }
cfb8 = { version = "0.4.0", optional = true }
rand = { version = "0.7.3", optional = true }
rsa = { version = "0.3.0", optional = true }

View File

@ -124,72 +124,78 @@ async fn interact_login(mut con: Connection<Login>) -> io::Result<()> {
}
};
use rand::Rng;
use rand::rngs::OsRng;
use rsa::{RSAPrivateKey, PaddingScheme};
#[cfg(feature = "encryption")]
{
use rand::Rng;
use rand::rngs::OsRng;
use rsa::{RSAPrivateKey, PaddingScheme};
use std::fs::File;
use std::io::Read;
let mut public_key = Vec::new();
File::open("private/pub.der").expect("missing public key").read_to_end(&mut public_key)?;
use std::fs::File;
use std::io::Read;
let mut public_key = Vec::new();
File::open("private/pub.der").expect("missing public key").read_to_end(&mut public_key)?;
let mut private_key = Vec::new();
File::open("private/priv.der").expect("missing private key").read_to_end(&mut private_key)?;
let mut private_key = Vec::new();
File::open("private/priv.der").expect("missing private key").read_to_end(&mut private_key)?;
let key = RSAPrivateKey::from_pkcs1(&private_key).expect("Invalid private key.");
let key = RSAPrivateKey::from_pkcs1(&private_key).expect("Invalid private key.");
let mut verify_token = Vec::new();
verify_token.resize(4, 0u8);
OsRng.fill(verify_token.as_mut_slice());
let mut verify_token = Vec::new();
verify_token.resize(4, 0u8);
OsRng.fill(verify_token.as_mut_slice());
let server_id = "";
let server_id = "";
con.write(&Clientbound::EncryptionRequest(EncryptionRequest {
server_id: server_id.to_string().into_boxed_str(),
public_key: public_key.clone().into_boxed_slice(),
verify_token: verify_token.clone().into_boxed_slice(),
})).await?;
con.write(&Clientbound::EncryptionRequest(EncryptionRequest {
server_id: server_id.to_string().into_boxed_str(),
public_key: public_key.clone().into_boxed_slice(),
verify_token: verify_token.clone().into_boxed_slice(),
})).await?;
let secret = match con.read().await? {
Serverbound::EncryptionResponse(encryption_response) => {
let token = key.decrypt(PaddingScheme::PKCS1v15Encrypt,
&encryption_response.verify_token)
.expect("Failed to decrypt verify token.");
if token.as_slice() != verify_token.as_slice() {
return mk_err("Incorrect verify token.");
let secret = match con.read().await? {
Serverbound::EncryptionResponse(encryption_response) => {
let token = key.decrypt(PaddingScheme::PKCS1v15Encrypt,
&encryption_response.verify_token)
.expect("Failed to decrypt verify token.");
if token.as_slice() != verify_token.as_slice() {
return mk_err("Incorrect verify token.");
}
key.decrypt(PaddingScheme::PKCS1v15Encrypt, &encryption_response.shared_secret)
.expect("Failed to decrypt shared secret.")
},
_ => {
return mk_err("Unexpected packet (expected Encryption Response).");
}
key.decrypt(PaddingScheme::PKCS1v15Encrypt, &encryption_response.shared_secret)
.expect("Failed to decrypt shared secret.")
},
_ => {
return mk_err("Unexpected packet (expected Encryption Response).");
}
};
con = con.set_encryption(&secret).expect("Failed to set encryption.");
use reqwest::Client;
let server_hash = {
let server_hash_bytes = {
use sha1::{Sha1, Digest};
let mut hasher = Sha1::new();
hasher.update(server_id.as_bytes());
hasher.update(&secret);
hasher.update(&public_key);
hasher.finalize()
};
format!("{:x}", num_bigint::BigInt::from_signed_bytes_be(&server_hash_bytes))
};
con = con.set_encryption(&secret).expect("Failed to set encryption.");
// TODO: Authentication, not just encryption.
println!("{:?}", Client::new().get("https://sessionserver.mojang.com/session/minecraft/hasJoined")
.header("Content-Type", "application/json")
.query(&[("username", name.clone()), ("serverId", server_hash.into_boxed_str())])
.send().await.expect("Request failed.").text().await.unwrap());
#[cfg(feature = "authentication")]
{
let server_hash = {
let server_hash_bytes = {
use sha1::{Sha1, Digest};
let mut hasher = Sha1::new();
hasher.update(server_id.as_bytes());
hasher.update(&secret);
hasher.update(&public_key);
hasher.finalize()
};
format!("{:x}", num_bigint::BigInt::from_signed_bytes_be(&server_hash_bytes))
};
use reqwest::Client;
println!("{:?}", Client::new().get("https://sessionserver.mojang.com/session/minecraft/hasJoined")
.header("Content-Type", "application/json")
.query(&[("username", name.clone()), ("serverId", server_hash.into_boxed_str())])
.send().await.expect("Request failed.").text().await.unwrap());
}
}
#[cfg(feature = "compression")]
con.set_compression(Some(64)).await?;
con.write(&Clientbound::LoginSuccess(LoginSuccess {

View File

@ -72,6 +72,7 @@ impl Connection<Handshake> {
}
impl Connection<Login> {
#[cfg(feature = "compression")]
pub async fn set_compression(&mut self, threshold: Option<u32>) -> io::Result<()> {
use crate::net::connection::packet_format::compressed::CompressedPacketFormat;
use crate::net::serialize::VarInt;
@ -99,6 +100,7 @@ impl Connection<Login> {
/// WARNING: This function is not idempontent.
/// Calling it twice will result in the underlying stream getting encrypted twice.
#[cfg(feature = "encryption")]
pub fn set_encryption(self, secret: &[u8]) -> Result<Self, String> {
use cfb8::Cfb8;
use cfb8::stream_cipher::NewStreamCipher;

View File

@ -1,3 +1,4 @@
#[cfg(feature = "compression")]
pub mod compressed;
pub mod default;

View File

@ -1,3 +1,4 @@
#[cfg(feature = "encryption")]
pub mod encrypted;
use tokio::io::{AsyncRead, AsyncWrite};