aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHimbeerserverDE <himbeerserverde@gmail.com>2024-01-22 18:57:14 +0100
committerHimbeerserverDE <himbeerserverde@gmail.com>2024-01-22 18:57:14 +0100
commitfb43b5c02b004ec6d34f19dd54befa1a9f058641 (patch)
tree55a83a9ad07387d5cb53a7315593ab283a9f0900
parent0b75144b6e13fce05f6ead7db6a58004bcc2194d (diff)
implement client-side authentication and encryption setup
-rw-r--r--Cargo.lock11
-rw-r--r--hbak_common/Cargo.toml2
-rw-r--r--hbak_common/src/conn.rs125
-rw-r--r--hbak_common/src/error.rs25
-rw-r--r--hbak_common/src/message.rs2
5 files changed, 152 insertions, 13 deletions
diff --git a/Cargo.lock b/Cargo.lock
index edcfc29..696dc8c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -100,6 +100,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
[[package]]
+name = "bincode"
+version = "1.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad"
+dependencies = [
+ "serde",
+]
+
+[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -326,6 +335,7 @@ name = "hbak_common"
version = "0.1.0-dev"
dependencies = [
"argon2",
+ "bincode",
"chacha20",
"chacha20poly1305",
"chrono",
@@ -333,6 +343,7 @@ dependencies = [
"rand",
"serde",
"sha2",
+ "subtle",
"sys-mount",
"thiserror",
"toml",
diff --git a/hbak_common/Cargo.toml b/hbak_common/Cargo.toml
index eac46a9..0006605 100644
--- a/hbak_common/Cargo.toml
+++ b/hbak_common/Cargo.toml
@@ -7,6 +7,7 @@ edition = "2021"
[dependencies]
argon2 = { version = "0.5.2", default-features = false, features = ["std"] }
+bincode = "1.3.3"
chacha20 = "0.9.1"
chacha20poly1305 = { version = "0.10.1", features = ["stream", "std"] }
chrono = { version = "0.4.31", features = ["serde"] }
@@ -14,6 +15,7 @@ hmac = "0.12.1"
rand = "0.8.5"
serde = { version = "1.0", features = ["derive"] }
sha2 = { version = "0.10.8", default-features = false }
+subtle = "2.5.0"
sys-mount = { version = "2.1.0", default-features = false }
thiserror = "1.0"
toml = "0.8.8"
diff --git a/hbak_common/src/conn.rs b/hbak_common/src/conn.rs
index 974aa63..c122007 100644
--- a/hbak_common/src/conn.rs
+++ b/hbak_common/src/conn.rs
@@ -1,21 +1,22 @@
-use crate::message::Target;
+use crate::message::*;
+use crate::system;
use crate::{NetworkError, RemoteError};
+use std::io::Write;
use std::net::{SocketAddr, TcpStream};
use std::time::Duration;
+use chacha20poly1305::aead::generic_array::GenericArray;
use chacha20poly1305::aead::stream::{DecryptorBE32, EncryptorBE32};
-use chacha20poly1305::XChaCha20Poly1305;
+use chacha20poly1305::{Key, XChaCha20Poly1305};
+use subtle::ConstantTimeEq;
/// TCP connect timeout. Connection attempt is aborted if remote doesn't respond.
const CONNECT_TIMEOUT: Duration = Duration::from_secs(30);
/// The valid states of an [`AuthConn`].
-#[derive(Debug, Default, Eq, PartialEq)]
+#[derive(Debug, Eq, PartialEq)]
enum AuthConnState {
- /// Authentication has not started.
- #[default]
- Idle,
/// A `Hello` message has been sent. Awaiting the `ServerAuth` response.
Handshake {
/// The challenge sent in the `Hello` message.
@@ -26,14 +27,11 @@ enum AuthConnState {
/// A `ServerAuth` message has been received and a `ClientAuth` reaction has been sent.
/// Awaiting the `Encrypt` response.
Proof {
+ /// The shared secret for mutual authentication and transport encryption.
+ key: Vec<u8>,
/// The nonce for transport encryption.
nonce: Vec<u8>,
},
- /// An `Encrypt` message has been received and encryption has been configured.
- /// Further plaintext reads or writes are not allowed. Transformation is imminent.
- Encrypted,
- /// Authentication or encryption setup has failed. The connection should be terminated.
- Failed(RemoteError),
}
/// The valid states of an [`AuthServ`].
@@ -117,13 +115,100 @@ impl AuthConn {
pub fn new(addr: &SocketAddr) -> Result<Self, NetworkError> {
Ok(TcpStream::connect_timeout(addr, CONNECT_TIMEOUT)?.into())
}
+
+ /// Performs mutual authentication and encryption of the connection
+ /// using the provided node name and passphrase,
+ /// returning a [`StreamConn`] on success.
+ pub fn secure_stream<P: AsRef<[u8]>>(
+ mut self,
+ node_name: String,
+ passphrase: P,
+ ) -> Result<StreamConn, NetworkError> {
+ // No need to check for `AuthConnState::Idle`, consuming the `AuthConn`
+ // guarantees that this function can never be called again.
+
+ // Limit variables to this scope so they aren't used in the main loop by accident.
+ {
+ let AuthConnState::Handshake { challenge, nonce } = &self.state else {
+ unreachable!()
+ };
+
+ self.send_message(&CryptoMessage::Hello(Hello {
+ node_name,
+ challenge: challenge.to_vec(),
+ nonce: nonce.to_vec(),
+ }))?;
+ }
+
+ loop {
+ let message = self.recv_message()?;
+
+ match (self.state, message) {
+ (
+ AuthConnState::Handshake { challenge, nonce },
+ CryptoMessage::ServerAuth(server_auth),
+ ) => {
+ let server_auth = server_auth?;
+
+ let key = system::derive_key(&server_auth.verifier, &passphrase)?;
+ let server_proof = system::hash_hmac(&key, &challenge);
+
+ if server_auth.proof.ct_eq(&server_proof).into() {
+ let proof = system::hash_hmac(&key, &server_auth.challenge);
+
+ self.state = AuthConnState::Proof { key, nonce };
+ self.send_message(&CryptoMessage::ClientAuth(Ok(ClientAuth { proof })))?;
+ } else {
+ self.state = AuthConnState::Handshake { challenge, nonce };
+ self.send_message(&CryptoMessage::ClientAuth(Err(
+ RemoteError::AccessDenied,
+ )))?;
+
+ return Err(RemoteError::Unauthorized.into());
+ }
+ }
+ (AuthConnState::Proof { key, nonce }, CryptoMessage::Encrypt(encrypt)) => {
+ encrypt?;
+ return Ok(StreamConn::from_conn(self.stream, key, nonce));
+ }
+ (AuthConnState::Handshake { challenge, nonce }, _) => {
+ self.state = AuthConnState::Handshake { challenge, nonce };
+ self.send_message(&CryptoMessage::ClientAuth(Err(
+ RemoteError::IllegalTransition,
+ )))?;
+
+ return Err(NetworkError::IllegalTransition);
+ }
+ (AuthConnState::Proof { key, nonce }, _) => {
+ self.state = AuthConnState::Proof { key, nonce };
+ self.send_message(&CryptoMessage::Error(RemoteError::IllegalTransition))?;
+
+ return Err(NetworkError::IllegalTransition);
+ }
+ }
+ }
+ }
+
+ fn send_message(&self, message: &CryptoMessage) -> Result<(), NetworkError> {
+ let buf = bincode::serialize(message)?;
+ (&self.stream).write_all(&buf)?;
+
+ Ok(())
+ }
+
+ fn recv_message(&self) -> Result<CryptoMessage, NetworkError> {
+ Ok(bincode::deserialize_from(&self.stream)?)
+ }
}
impl From<TcpStream> for AuthConn {
fn from(stream: TcpStream) -> Self {
Self {
stream,
- state: AuthConnState::default(),
+ state: AuthConnState::Handshake {
+ challenge: system::random_bytes(32),
+ nonce: system::random_bytes(32),
+ },
}
}
}
@@ -154,3 +239,19 @@ pub struct StreamConn {
decryptor: DecryptorBE32<XChaCha20Poly1305>,
state: StreamConnState,
}
+
+impl StreamConn {
+ /// Constructs a new `StreamConn` from a [`std::net::TcpStream`],
+ /// encryption key and nonce.
+ pub(crate) fn from_conn(stream: TcpStream, key: Vec<u8>, nonce: Vec<u8>) -> Self {
+ let key = Key::from_slice(&key);
+ let nonce = GenericArray::from_slice(&nonce);
+
+ Self {
+ stream,
+ encryptor: EncryptorBE32::new(key, nonce),
+ decryptor: DecryptorBE32::new(key, nonce),
+ state: StreamConnState::default(),
+ }
+ }
+}
diff --git a/hbak_common/src/error.rs b/hbak_common/src/error.rs
index afee0ed..46dd345 100644
--- a/hbak_common/src/error.rs
+++ b/hbak_common/src/error.rs
@@ -127,6 +127,12 @@ pub enum LocalNodeError {
/// It may be a low-level connection issue or a high-level protocol error.
#[derive(Debug, Error)]
pub enum NetworkError {
+ /// A network reception represents an illegal state transition on the local node.
+ #[error("Illegal state transition")]
+ IllegalTransition,
+ /// An error occured on the local node.
+ #[error("Local error: {0}")]
+ LocalError(#[from] LocalNodeError),
/// A high-level `RemoteError` occured.
#[error("Remote error: {0}")]
RemoteError(#[from] RemoteError),
@@ -134,9 +140,26 @@ pub enum NetworkError {
/// A `std::io::Error` I/O error occured.
#[error("IO error: {0}")]
IoError(#[from] io::Error),
+
+ /// A bincode (de)serialization error occured.
+ #[error("Bincode (de)serialization error: {0}")]
+ Bincode(#[from] Box<bincode::ErrorKind>),
}
/// A `RemoteError` indicates an error condition on the current session
/// or the remote node. This is a special case of [`NetworkError`].
#[derive(Clone, Debug, Eq, PartialEq, Error, Serialize, Deserialize)]
-pub enum RemoteError {}
+pub enum RemoteError {
+ /// Access is denied by the remote node.
+ /// May be an authentication or authorization failure, infer details from the context.
+ #[error("Access denied by remote node")]
+ AccessDenied,
+ /// The remote node was denied access.
+ /// May be an authentication or authorization failure, infer details from the context.
+ #[error("Remote node is unauthorized")]
+ Unauthorized,
+
+ /// A network transmission represents an illegal state transition on the remote node.
+ #[error("Illegal state transition on remote node")]
+ IllegalTransition,
+}
diff --git a/hbak_common/src/message.rs b/hbak_common/src/message.rs
index 57fdbfc..8d8557c 100644
--- a/hbak_common/src/message.rs
+++ b/hbak_common/src/message.rs
@@ -19,6 +19,8 @@ pub enum CryptoMessage {
ClientAuth(Result<ClientAuth, RemoteError>),
/// Authentication successful. Further traffic is encrypted. This message is clientbound.
Encrypt(Result<(), RemoteError>),
+ /// Protocol error independent of the operation or state context.
+ Error(RemoteError),
}
/// Start the authentication process. This message is serverbound.