diff options
-rw-r--r-- | Cargo.lock | 11 | ||||
-rw-r--r-- | hbak_common/Cargo.toml | 2 | ||||
-rw-r--r-- | hbak_common/src/conn.rs | 125 | ||||
-rw-r--r-- | hbak_common/src/error.rs | 25 | ||||
-rw-r--r-- | hbak_common/src/message.rs | 2 |
5 files changed, 152 insertions, 13 deletions
@@ -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. |