summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/bin/client/connection.rs27
-rw-r--r--src/bin/client/database.rs64
-rw-r--r--src/bin/client/repl.rs126
-rw-r--r--src/bin/server/database.rs31
-rw-r--r--src/bin/server/handler.rs57
-rw-r--r--src/bin/server/main.rs1
-rw-r--r--src/bin/server/mod.rs1
7 files changed, 300 insertions, 7 deletions
diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs
index d03dc1a..497ed36 100644
--- a/src/bin/client/connection.rs
+++ b/src/bin/client/connection.rs
@@ -3,8 +3,18 @@ use std::net::TcpStream;
3use std::sync::mpsc; 3use std::sync::mpsc;
4use std::thread; 4use std::thread;
5use std::time::{Duration, Instant}; 5use std::time::{Duration, Instant};
6use rusqlite::Connection;
7use bcrypt::hash_with_salt;
8use crate::database;
6 9
7pub fn connect(addr: &str) -> Result<TcpStream, Box<dyn std::error::Error>> { 10const SALT: [u8; 16] = *b"fixedsalt1234567";
11
12fn hash_password(password: &str) -> String {
13 let result = hash_with_salt(password, 10, SALT).unwrap();
14 result.to_string()
15}
16
17pub fn connect(addr: &str, conn: &Connection) -> Result<TcpStream, Box<dyn std::error::Error>> {
8 let (tx, rx) = mpsc::channel(); 18 let (tx, rx) = mpsc::channel();
9 let addr_str = addr.to_string(); 19 let addr_str = addr.to_string();
10 thread::spawn(move || { 20 thread::spawn(move || {
@@ -23,6 +33,21 @@ pub fn connect(addr: &str) -> Result<TcpStream, Box<dyn std::error::Error>> {
23 ping_reader.read_line(&mut response).unwrap(); 33 ping_reader.read_line(&mut response).unwrap();
24 let elapsed = start.elapsed(); 34 let elapsed = start.elapsed();
25 println!("\n\rConnected to {} with ping delay: {:?}", addr, elapsed); 35 println!("\n\rConnected to {} with ping delay: {:?}", addr, elapsed);
36
37 // Check for credentials and auto-auth
38 if let Some((username, password)) = database::get_credentials(conn, addr) {
39 let hashed = hash_password(&password);
40 let auth_msg = format!("AUTH {} {}", username, hashed);
41 writeln!(ping_writer, "{}", auth_msg).unwrap();
42 let mut auth_response = String::new();
43 ping_reader.read_line(&mut auth_response).unwrap();
44 if auth_response.trim().starts_with("OK") {
45 println!("Authenticated as {}", username);
46 } else {
47 println!("Authentication failed: {}", auth_response.trim());
48 // Do not fail, allow manual login
49 }
50 }
26 Ok(s) 51 Ok(s)
27 } 52 }
28 Ok(Err(e)) => Err(Box::new(e)), 53 Ok(Err(e)) => Err(Box::new(e)),
diff --git a/src/bin/client/database.rs b/src/bin/client/database.rs
index bc8f6f8..03a6c45 100644
--- a/src/bin/client/database.rs
+++ b/src/bin/client/database.rs
@@ -1,4 +1,10 @@
1use rusqlite::Connection; 1use rusqlite::Connection;
2use aes_gcm::{Aes256Gcm, Key, Nonce};
3use aes_gcm::aead::{Aead, KeyInit};
4use pbkdf2::pbkdf2;
5use hmac::Hmac;
6use sha2::Sha256;
7use rand::Rng;
2 8
3pub fn init_db() -> Connection { 9pub fn init_db() -> Connection {
4 let conn = Connection::open("client.db").unwrap(); 10 let conn = Connection::open("client.db").unwrap();
@@ -9,6 +15,14 @@ pub fn init_db() -> Connection {
9 )", 15 )",
10 [], 16 [],
11 ).unwrap(); 17 ).unwrap();
18 conn.execute(
19 "CREATE TABLE IF NOT EXISTS credentials (
20 host TEXT PRIMARY KEY,
21 username TEXT NOT NULL,
22 encrypted_password BLOB NOT NULL
23 )",
24 [],
25 ).unwrap();
12 conn 26 conn
13} 27}
14 28
@@ -28,3 +42,53 @@ pub fn save_host(conn: &Connection, host: &str) {
28 [host], 42 [host],
29 ).unwrap(); 43 ).unwrap();
30} 44}
45
46pub fn save_credentials(conn: &Connection, host: &str, username: &str, password: &str) -> Result<(), aes_gcm::Error> {
47 let encrypted = encrypt_password(password)?;
48 conn.execute(
49 "INSERT OR REPLACE INTO credentials (host, username, encrypted_password) VALUES (?1, ?2, ?3)",
50 (host, username, &encrypted),
51 ).unwrap();
52 Ok(())
53}
54
55pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String)> {
56 let mut stmt = conn.prepare("SELECT username, encrypted_password FROM credentials WHERE host = ?1").unwrap();
57 let mut rows = stmt.query([host]).unwrap();
58 if let Ok(Some(row)) = rows.next() {
59 let username: String = row.get(0).unwrap();
60 let encrypted: Vec<u8> = row.get(1).unwrap();
61 decrypt_password(&encrypted).map(|password| (username, password))
62 } else {
63 None
64 }
65}
66
67fn derive_key() -> Key<Aes256Gcm> {
68 let mut key = [0u8; 32];
69 let salt = b"client_salt"; // Fixed salt for simplicity
70 pbkdf2::<Hmac<Sha256>>(b"client_key", salt, 10000, &mut key).expect("error deriving key");
71 key.into()
72}
73
74fn encrypt_password(password: &str) -> Result<Vec<u8>, aes_gcm::Error> {
75 let key = derive_key();
76 let cipher = Aes256Gcm::new(&key);
77 let mut nonce_bytes = [0u8; 12];
78 rand::thread_rng().fill(&mut nonce_bytes);
79 let nonce = Nonce::from_slice(&nonce_bytes);
80 let ciphertext = cipher.encrypt(nonce, password.as_bytes())?;
81 let mut result = nonce_bytes.to_vec();
82 result.extend(ciphertext);
83 Ok(result)
84}
85
86fn decrypt_password(encrypted: &[u8]) -> Option<String> {
87 if encrypted.len() < 12 {
88 return None;
89 }
90 let key = derive_key();
91 let cipher = Aes256Gcm::new(&key);
92 let nonce = Nonce::from_slice(&encrypted[..12]);
93 cipher.decrypt(nonce, &encrypted[12..]).ok().and_then(|plaintext| String::from_utf8(plaintext).ok())
94}
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs
index 1512091..5559cfd 100644
--- a/src/bin/client/repl.rs
+++ b/src/bin/client/repl.rs
@@ -2,10 +2,18 @@ use std::io::{BufRead, BufReader, Write, stdout};
2use std::net::TcpStream; 2use std::net::TcpStream;
3use rusqlite::Connection; 3use rusqlite::Connection;
4use std::time::Instant; 4use std::time::Instant;
5use bcrypt::hash_with_salt;
5 6
6use crate::connection; 7use crate::connection;
7use crate::database; 8use crate::database;
8 9
10const SALT: [u8; 16] = *b"fixedsalt1234567";
11
12fn hash_password(password: &str) -> String {
13 let result = hash_with_salt(password, 10, SALT).unwrap();
14 result.to_string()
15}
16
9use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show}, execute}; 17use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show}, execute};
10 18
11macro_rules! println { 19macro_rules! println {
@@ -47,7 +55,7 @@ impl ClientState {
47 55
48 pub fn connect_to(&mut self, addr: &str) { 56 pub fn connect_to(&mut self, addr: &str) {
49 self.addr = Some(addr.to_string()); 57 self.addr = Some(addr.to_string());
50 match connection::connect(addr) { 58 match connection::connect(addr, &self.conn) {
51 Ok(s) => { 59 Ok(s) => {
52 self.stream = Some(s); 60 self.stream = Some(s);
53 database::save_host(&self.conn, addr); 61 database::save_host(&self.conn, addr);
@@ -166,6 +174,108 @@ impl ClientState {
166 } 174 }
167 } 175 }
168 } 176 }
177 "register" => {
178 if self.stream.is_none() {
179 println!("\nNot connected. Use 'connect <ip>:<port>' first.");
180 continue 'outer;
181 }
182 let username = read_line("\nUsername: ");
183 if username.is_empty() {
184 continue 'outer;
185 }
186 let password = read_line("Password: ");
187 if password.is_empty() {
188 continue 'outer;
189 }
190 let hashed = hash_password(&password);
191 let msg = format!("REGISTER {} {}", username, hashed);
192 let s = self.stream.as_mut().unwrap();
193 let writer_stream = s.try_clone().unwrap();
194 let mut writer = writer_stream;
195 let response_reader_stream = s.try_clone().unwrap();
196 let mut response_reader = BufReader::new(response_reader_stream);
197 if let Err(_) = writeln!(writer, "{}", msg) {
198 self.stream = None;
199 println!("\nConnection lost");
200 continue 'outer;
201 }
202 let mut response = String::new();
203 if let Err(_) = response_reader.read_line(&mut response) {
204 self.stream = None;
205 println!("\nConnection lost");
206 } else {
207 println!("\n{}", response.trim());
208 }
209 }
210 "login" => {
211 if self.stream.is_none() {
212 println!("\nNot connected. Use 'connect <ip>:<port>' first.");
213 continue 'outer;
214 }
215 loop {
216 let username = read_line("\nUsername: ");
217 if username.is_empty() {
218 break;
219 }
220 let password = read_line("Password: ");
221 if password.is_empty() {
222 break;
223 }
224 let hashed = hash_password(&password);
225 let msg = format!("AUTH {} {}", username, hashed);
226 let s = self.stream.as_mut().unwrap();
227 let writer_stream = s.try_clone().unwrap();
228 let mut writer = writer_stream;
229 let response_reader_stream = s.try_clone().unwrap();
230 let mut response_reader = BufReader::new(response_reader_stream);
231 if let Err(_) = writeln!(writer, "{}", msg) {
232 self.stream = None;
233 println!("\nConnection lost");
234 break;
235 }
236 let mut response = String::new();
237 if let Err(_) = response_reader.read_line(&mut response) {
238 self.stream = None;
239 println!("\nConnection lost");
240 break;
241 } else {
242 let resp = response.trim();
243 println!("\n{}", resp);
244 if resp.starts_with("OK") {
245 // Save credentials
246 if let Some(addr) = &self.addr {
247 if let Err(e) = database::save_credentials(&self.conn, addr, &username, &password) {
248 println!("Failed to save credentials: {}", e);
249 }
250 }
251 break;
252 } else {
253 println!("Authentication failed, try again or press Enter to cancel.");
254 }
255 }
256 }
257 }
258 "setcred" => {
259 if self.addr.is_none() {
260 println!("\nNo host connected. Use 'connect <ip>:<port>' first.");
261 continue 'outer;
262 }
263 let username = read_line("\nUsername: ");
264 if username.is_empty() {
265 continue 'outer;
266 }
267 let password = read_line("Password: ");
268 if password.is_empty() {
269 continue 'outer;
270 }
271 if let Some(addr) = &self.addr {
272 if let Err(e) = database::save_credentials(&self.conn, addr, &username, &password) {
273 println!("Failed to save credentials: {}", e);
274 } else {
275 println!("Credentials saved for {}", addr);
276 }
277 }
278 }
169 "help" => { 279 "help" => {
170 help(); 280 help();
171 } 281 }
@@ -208,6 +318,18 @@ fn redraw(current_input: &str) {
208 execute!(stdout(), MoveToColumn((2 + current_input.len()) as u16)).unwrap(); 318 execute!(stdout(), MoveToColumn((2 + current_input.len()) as u16)).unwrap();
209} 319}
210 320
321fn read_line(prompt: &str) -> String {
322 disable_raw_mode().unwrap();
323 execute!(stdout(), Show).unwrap();
324 print!("{}", prompt);
325 stdout().flush().unwrap();
326 let mut input = String::new();
327 std::io::stdin().read_line(&mut input).unwrap();
328 enable_raw_mode().unwrap();
329 execute!(stdout(), crossterm::cursor::Hide).unwrap();
330 input.trim().to_string()
331}
332
211fn help() { 333fn help() {
212 println!("Available commands: connect <ip>:<port>, send <message>, disconnect, ping, help, quit"); 334 println!("Available commands: connect <ip>:<port>, send <message>, disconnect, ping, login, register, setcred, help, quit");
213} 335}
diff --git a/src/bin/server/database.rs b/src/bin/server/database.rs
new file mode 100644
index 0000000..9c51997
--- /dev/null
+++ b/src/bin/server/database.rs
@@ -0,0 +1,31 @@
1use rusqlite::Connection;
2
3pub fn init_db() -> Connection {
4 let conn = Connection::open("server.db").unwrap();
5 conn.execute(
6 "CREATE TABLE IF NOT EXISTS users (
7 username TEXT PRIMARY KEY,
8 hashed_password TEXT NOT NULL
9 )",
10 [],
11 ).unwrap();
12 conn
13}
14
15pub fn add_user(conn: &Connection, username: &str, hashed_password: &str) -> Result<(), Box<dyn std::error::Error>> {
16 conn.execute(
17 "INSERT INTO users (username, hashed_password) VALUES (?1, ?2)",
18 [username, hashed_password],
19 )?;
20 Ok(())
21}
22
23pub fn get_hashed_password(conn: &Connection, username: &str) -> Option<String> {
24 let mut stmt = conn.prepare("SELECT hashed_password FROM users WHERE username = ?1").unwrap();
25 let mut rows = stmt.query([username]).unwrap();
26 if let Ok(Some(row)) = rows.next() {
27 row.get(0).ok()
28 } else {
29 None
30 }
31}
diff --git a/src/bin/server/handler.rs b/src/bin/server/handler.rs
index 906a91f..2132d09 100644
--- a/src/bin/server/handler.rs
+++ b/src/bin/server/handler.rs
@@ -1,15 +1,64 @@
1use std::io::{BufRead, BufReader, Write}; 1use std::io::{BufRead, BufReader, Write};
2use std::net::TcpStream; 2use std::net::TcpStream;
3use std::sync::Mutex;
4use once_cell::sync::Lazy;
5use rusqlite::Connection;
6use crate::database;
7
8static DB_CONN: Lazy<Mutex<Connection>> = Lazy::new(|| Mutex::new(database::init_db()));
3 9
4pub fn handle_client(stream: TcpStream) { 10pub fn handle_client(stream: TcpStream) {
5 println!("Client connected"); 11 let client_addr = stream.peer_addr().unwrap().to_string();
12 let mut user = "guest".to_string();
13 println!("[{} @ {}] Client connected", user, client_addr);
6 let reader_stream = stream.try_clone().unwrap(); 14 let reader_stream = stream.try_clone().unwrap();
7 let reader = BufReader::new(reader_stream); 15 let reader = BufReader::new(reader_stream);
8 let mut writer = stream; 16 let mut writer = stream;
9 for line in reader.lines() { 17 for line in reader.lines() {
10 let line = line.unwrap(); 18 let line = line.unwrap();
11 println!("Received: {}", line); 19 println!("[{} @ {}] Received: {}", user, client_addr, line);
12 writeln!(writer, "{}", line).unwrap(); 20 let response = process_message(&line, &mut user);
21 writeln!(writer, "{}", response).unwrap();
22 }
23 println!("[{} @ {}] Client disconnected", user, client_addr);
24}
25
26fn process_message(msg: &str, user: &mut String) -> String {
27 let parts: Vec<&str> = msg.splitn(3, ' ').collect();
28 match parts[0] {
29 "REGISTER" => {
30 if parts.len() != 3 {
31 "FAIL Invalid REGISTER format".to_string()
32 } else {
33 let username = parts[1];
34 let hashed_pass = parts[2];
35 let db = DB_CONN.lock().unwrap();
36 match database::add_user(&db, username, hashed_pass) {
37 Ok(_) => format!("OK Registered {}", username),
38 Err(_) => "FAIL User already exists".to_string(),
39 }
40 }
41 }
42 "AUTH" => {
43 if parts.len() != 3 {
44 "FAIL Invalid AUTH format".to_string()
45 } else {
46 let username = parts[1];
47 let provided_hash = parts[2];
48 let db = DB_CONN.lock().unwrap();
49 if let Some(stored_hash) = database::get_hashed_password(&db, username) {
50 if provided_hash == stored_hash {
51 *user = username.to_string();
52 format!("OK {}", username)
53 } else {
54 "FAIL Invalid credentials".to_string()
55 }
56 } else {
57 "FAIL User not found".to_string()
58 }
59 }
60 }
61 "PING" => "PING".to_string(),
62 _ => msg.to_string(), // Echo back for compatibility
13 } 63 }
14 println!("Client disconnected");
15} 64}
diff --git a/src/bin/server/main.rs b/src/bin/server/main.rs
index 2c6e1dc..7693069 100644
--- a/src/bin/server/main.rs
+++ b/src/bin/server/main.rs
@@ -1,5 +1,6 @@
1mod args; 1mod args;
2mod handler; 2mod handler;
3mod database;
3 4
4use std::net::TcpListener; 5use std::net::TcpListener;
5use std::thread; 6use std::thread;
diff --git a/src/bin/server/mod.rs b/src/bin/server/mod.rs
index adb60bb..b2d622e 100644
--- a/src/bin/server/mod.rs
+++ b/src/bin/server/mod.rs
@@ -1,2 +1,3 @@
1pub mod args; 1pub mod args;
2pub mod handler; 2pub mod handler;
3pub mod database;