Building a Secure, Real-Time CLI Chat App with Rust A step-by-step guide to creating a secure, real-time CLI chat application using Rust, featuring PostgreSQL integration, JWT authentication, and WebSocket for instant messaging.
This blog will walk you through creating a command-line interface (CLI) chat application that's not only real-time but also prioritizes security at its core. Using Rust's powerful ecosystem, we'll harness the robustness of PostgreSQL for our database needs, implement JWT for secure authentication, and utilize WebSockets for that instant, real-time chat experience. Whether you're here to expand your Rust skills or build a secure communication tool, you're in the right place. Let's get started!
Initialize a new Rust project:
cargo new rust_cli_chat
cd rust_cli_chat
Add dependencies to Cargo.toml
:
[ dependencies ]
tokio = { version = "1" , features = [ "full" ] }
diesel = { version = "1.4.5" , features = [ "postgres" ] }
serde = { version = "1.0" , features = [ "derive" ] }
serde_json = "1.0"
argon2 = "0.3.0"
jwt = "0.9.0"
aes-gcm = "0.11"
tokio-tungstenite = "0.17.2"
cargo install diesel_cli --no-default-features --features postgres
diesel setup
Create migrations for users and messages:
diesel migration generate create_users
diesel migration generate create_messages
-- For create_users
CREATE TABLE users (
id SERIAL PRIMARY KEY ,
username VARCHAR NOT NULL UNIQUE ,
password_hash VARCHAR NOT NULL
);
-- For create_messages
CREATE TABLE messages (
id SERIAL PRIMARY KEY ,
sender_id INTEGER REFERENCES users(id),
content TEXT NOT NULL ,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
diesel migration run
// src/models.rs
use diesel :: prelude ::* ;
#[derive( Queryable , Insertable )]
#[diesel(table_name = users)]
pub struct User {
pub id : i32 ,
pub username : String ,
pub password_hash : String ,
}
pub struct NewUser <' a > {
pub username : & ' a str ,
pub password : & ' a str ,
}
impl User {
pub fn hash_password (password : & str ) -> Result < String , Box < dyn std :: error :: Error >> {
use argon2 :: Config ;
let config = Config :: default ();
Ok ( argon2 :: hash_encoded (password . as_bytes (), & [], & config) ? )
}
pub fn verify_password ( & self , password : & str ) -> bool {
argon2 :: verify_encoded ( & self . password_hash, password . as_bytes ()) . unwrap_or ( false )
}
}
JWT creation and verification:
// src/auth.rs
use jsonwebtoken :: {encode, Header , Validation };
pub struct Claims {
pub sub : String ,
pub exp : usize ,
}
impl Claims {
pub fn new (user_id : & str ) -> Self {
use chrono :: Utc ;
let exp = ( Utc :: now () . timestamp () as usize ) + 3600 ; // 1 hour expiration
Claims {
sub : user_id . to_string (),
exp,
}
}
}
pub fn create_token (user_id : & str ) -> String {
encode ( & Header :: default (), & Claims :: new (user_id), & EncodingKey :: from_secret ( "your_secret_key" . as_ref ())) . unwrap ()
}
// src/crypto.rs
use aes_gcm :: {
aead :: { Aead , NewAead },
Aes256Gcm , Nonce , // Or use Aes128Gcm if you prefer 128 bit key
};
pub fn encrypt (plaintext : & str , key : & [ u8 ]) -> Vec < u8 > {
let cipher = Aes256Gcm :: new_from_slice (key) . expect ( "Key length must be 256 bits for AES-256" );
let nonce = Nonce :: from_slice ( & [ 0 u8 ; 12 ]); // 96-bits; unique per message
cipher . encrypt (nonce, plaintext . as_ref ()) . expect ( "Encryption failed" )
}
pub fn decrypt (ciphertext : & [ u8 ], key : & [ u8 ]) -> Result < String , Box < dyn std :: error :: Error >> {
let cipher = Aes256Gcm :: new_from_slice (key) ? ;
let nonce = Nonce :: from_slice ( & [ 0 u8 ; 12 ]);
let decrypted = cipher . decrypt (nonce, ciphertext . as_ref ()) ? ;
Ok ( String :: from_utf8 (decrypted) ? )
}
// src/websocket.rs
use tokio_tungstenite :: {accept_async, tungstenite :: Error };
use futures_util :: { StreamExt , SinkExt };
use tokio :: net :: TcpListener ;
use std :: env;
#[tokio :: main]
async fn main () -> Result <(), Error > {
let addr = env :: var ( "LISTEN_ADDR" ) . unwrap_or ( "127.0.0.1:8080" . to_string ());
let listener = TcpListener :: bind ( & addr) .await? ;
println! ( "Server listening on {}" , addr);
while let Ok ((stream, _)) = listener . accept () .await {
tokio :: spawn ( handle_connection (stream));
}
Ok (())
}
async fn handle_connection (stream : tokio :: net :: TcpStream ) {
let ws_stream = accept_async (stream) .await. expect ( "Failed to accept" );
let ( mut sender, mut receiver) = ws_stream . split ();
while let Some (msg) = receiver . next () .await {
match msg {
Ok (m) if m . is_text () => {
println! ( "Received: {:?}" , m);
// Here, decrypt the message, process it, and broadcast to other clients
sender . send (m) .await. expect ( "Failed to send" );
},
Err (e) => {
println! ( "Error processing message: {:?}" , e);
break ;
},
_ => (), // Ignore other message types for simplicity
}
}
}
// src/main.rs
use std :: io :: { self , Write };
fn main () -> io :: Result <()> {
let mut username = String :: new ();
loop {
print! ( "(login/send/quit) > " );
io :: stdout () . flush () ? ;
let mut input = String :: new ();
io :: stdin () . read_line ( &mut input) ? ;
match input . trim () {
"login" => {
// Implement login logic here, get username and password
username = input . trim () . split ( ' ' ) . nth ( 1 ) . unwrap () . to_string ();
let token = auth :: create_token ( & username); // Assuming auth.rs contains token creation
println! ( "Logged in as {}. Token: {}" , username, token);
},
"send" => {
if username . is_empty () {
println! ( "Please login first." );
continue ;
}
// Implement message sending logic here
},
"quit" => break ,
_ => println! ( "Unknown command" ),
}
}
Ok (())
}
cargo run
This guide provides a foundational setup for your CLI chat application, focusing on security with encrypted messages, real-time capabilities with WebSockets, and a solid database structure using Diesel with PostgreSQL. Each component needs further development:
Connection pooling for PostgreSQL to manage multiple client connections efficiently.
Error Handling should be robust, handling all potential failures gracefully.
CLI commands need to handle message sending and receiving properly with WebSocket integration.
Security improvements like SSL for WebSocket connections, more sophisticated JWT handling, etc.
This setup gives you a skeleton to build upon, ensuring your chat app is secure, real-time, and scalable with Rust's performance capabilities.