summaryrefslogtreecommitdiffstats
path: root/src/bin/client
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/bin/client/connection.rs19
-rw-r--r--src/bin/client/database.rs54
-rw-r--r--src/bin/client/macros.rs8
-rw-r--r--src/bin/client/main.rs2
-rw-r--r--src/bin/client/repl.rs156
5 files changed, 151 insertions, 88 deletions
diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs
index 497ed36..b07bf01 100644
--- a/src/bin/client/connection.rs
+++ b/src/bin/client/connection.rs
@@ -4,17 +4,9 @@ use std::sync::mpsc;
4use std::thread; 4use std::thread;
5use std::time::{Duration, Instant}; 5use std::time::{Duration, Instant};
6use rusqlite::Connection; 6use rusqlite::Connection;
7use bcrypt::hash_with_salt;
8use crate::database; 7use crate::database;
9 8
10const SALT: [u8; 16] = *b"fixedsalt1234567"; 9pub fn connect(addr: &str, conn: &Connection) -> Result<(TcpStream, Option<String>), Box<dyn std::error::Error>> {
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>> {
18 let (tx, rx) = mpsc::channel(); 10 let (tx, rx) = mpsc::channel();
19 let addr_str = addr.to_string(); 11 let addr_str = addr.to_string();
20 thread::spawn(move || { 12 thread::spawn(move || {
@@ -36,19 +28,20 @@ pub fn connect(addr: &str, conn: &Connection) -> Result<TcpStream, Box<dyn std::
36 28
37 // Check for credentials and auto-auth 29 // Check for credentials and auto-auth
38 if let Some((username, password)) = database::get_credentials(conn, addr) { 30 if let Some((username, password)) = database::get_credentials(conn, addr) {
39 let hashed = hash_password(&password); 31 let auth_msg = format!("AUTH {} {}", username, password);
40 let auth_msg = format!("AUTH {} {}", username, hashed);
41 writeln!(ping_writer, "{}", auth_msg).unwrap(); 32 writeln!(ping_writer, "{}", auth_msg).unwrap();
42 let mut auth_response = String::new(); 33 let mut auth_response = String::new();
43 ping_reader.read_line(&mut auth_response).unwrap(); 34 ping_reader.read_line(&mut auth_response).unwrap();
44 if auth_response.trim().starts_with("OK") { 35 if auth_response.trim().starts_with("OK") {
45 println!("Authenticated as {}", username); 36 println!("Authenticated as {}", username);
37 Ok((s, Some(username)))
46 } else { 38 } else {
47 println!("Authentication failed: {}", auth_response.trim()); 39 println!("Authentication failed: {}", auth_response.trim());
48 // Do not fail, allow manual login 40 Ok((s, None))
49 } 41 }
42 } else {
43 Ok((s, None))
50 } 44 }
51 Ok(s)
52 } 45 }
53 Ok(Err(e)) => Err(Box::new(e)), 46 Ok(Err(e)) => Err(Box::new(e)),
54 Err(mpsc::RecvTimeoutError::Timeout) => Err("Connection timed out after 3 seconds".into()), 47 Err(mpsc::RecvTimeoutError::Timeout) => Err("Connection timed out after 3 seconds".into()),
diff --git a/src/bin/client/database.rs b/src/bin/client/database.rs
index 03a6c45..da069a7 100644
--- a/src/bin/client/database.rs
+++ b/src/bin/client/database.rs
@@ -1,10 +1,4 @@
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;
8 2
9pub fn init_db() -> Connection { 3pub fn init_db() -> Connection {
10 let conn = Connection::open("client.db").unwrap(); 4 let conn = Connection::open("client.db").unwrap();
@@ -19,7 +13,7 @@ pub fn init_db() -> Connection {
19 "CREATE TABLE IF NOT EXISTS credentials ( 13 "CREATE TABLE IF NOT EXISTS credentials (
20 host TEXT PRIMARY KEY, 14 host TEXT PRIMARY KEY,
21 username TEXT NOT NULL, 15 username TEXT NOT NULL,
22 encrypted_password BLOB NOT NULL 16 password TEXT NOT NULL
23 )", 17 )",
24 [], 18 [],
25 ).unwrap(); 19 ).unwrap();
@@ -43,52 +37,22 @@ pub fn save_host(conn: &Connection, host: &str) {
43 ).unwrap(); 37 ).unwrap();
44} 38}
45 39
46pub fn save_credentials(conn: &Connection, host: &str, username: &str, password: &str) -> Result<(), aes_gcm::Error> { 40pub fn save_credentials(conn: &Connection, host: &str, username: &str, password: &str) -> Result<(), Box<dyn std::error::Error>> {
47 let encrypted = encrypt_password(password)?;
48 conn.execute( 41 conn.execute(
49 "INSERT OR REPLACE INTO credentials (host, username, encrypted_password) VALUES (?1, ?2, ?3)", 42 "INSERT OR REPLACE INTO credentials (host, username, password) VALUES (?1, ?2, ?3)",
50 (host, username, &encrypted), 43 (host, username, password),
51 ).unwrap(); 44 )?;
52 Ok(()) 45 Ok(())
53} 46}
54 47
55pub fn get_credentials(conn: &Connection, host: &str) -> Option<(String, String)> { 48pub 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(); 49 let mut stmt = conn.prepare("SELECT username, password FROM credentials WHERE host = ?1").unwrap();
57 let mut rows = stmt.query([host]).unwrap(); 50 let mut rows = stmt.query([host]).unwrap();
58 if let Ok(Some(row)) = rows.next() { 51 if let Ok(Some(row)) = rows.next() {
59 let username: String = row.get(0).unwrap(); 52 let username: String = row.get(0).unwrap();
60 let encrypted: Vec<u8> = row.get(1).unwrap(); 53 let password: String = row.get(1).unwrap();
61 decrypt_password(&encrypted).map(|password| (username, password)) 54 Some((username, password))
62 } else { 55 } else {
63 None 56 None
64 } 57 }
65} 58} \ No newline at end of file
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/macros.rs b/src/bin/client/macros.rs
new file mode 100644
index 0000000..f97a4ff
--- /dev/null
+++ b/src/bin/client/macros.rs
@@ -0,0 +1,8 @@
1macro_rules! println {
2 () => {
3 print!("\r\n")
4 };
5 ($($arg:tt)*) => {
6 print!("\r{}\n", format_args!($($arg)*))
7 };
8}
diff --git a/src/bin/client/main.rs b/src/bin/client/main.rs
index ce9c92f..9c15e9e 100644
--- a/src/bin/client/main.rs
+++ b/src/bin/client/main.rs
@@ -1,3 +1,5 @@
1#[macro_use]
2mod macros;
1mod repl; 3mod repl;
2mod database; 4mod database;
3mod connection; 5mod connection;
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs
index 5559cfd..b2f6ecd 100644
--- a/src/bin/client/repl.rs
+++ b/src/bin/client/repl.rs
@@ -2,28 +2,13 @@ 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;
6 5
7use crate::connection; 6use crate::connection;
8use crate::database; 7use crate::database;
9 8
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
17use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show}, execute}; 9use crossterm::{event::{read, Event, KeyCode, KeyModifiers}, terminal::{disable_raw_mode, enable_raw_mode, Clear, ClearType}, cursor::{MoveToColumn, Show}, execute};
18 10
19macro_rules! println { 11use sha2::{Sha256, Digest};
20 () => {
21 print!("\r\n")
22 };
23 ($($arg:tt)*) => {
24 print!("\r{}\n", format_args!($($arg)*))
25 };
26}
27 12
28pub struct ClientState { 13pub struct ClientState {
29 pub stream: Option<TcpStream>, 14 pub stream: Option<TcpStream>,
@@ -32,6 +17,7 @@ pub struct ClientState {
32 pub default_port: u16, 17 pub default_port: u16,
33 pub history: Vec<String>, 18 pub history: Vec<String>,
34 pub history_index: usize, 19 pub history_index: usize,
20 pub logged_in_user: Option<String>,
35} 21}
36 22
37impl ClientState { 23impl ClientState {
@@ -50,14 +36,16 @@ impl ClientState {
50 default_port, 36 default_port,
51 history: vec![], 37 history: vec![],
52 history_index: 0, 38 history_index: 0,
39 logged_in_user: None,
53 } 40 }
54 } 41 }
55 42
56 pub fn connect_to(&mut self, addr: &str) { 43 pub fn connect_to(&mut self, addr: &str) {
57 self.addr = Some(addr.to_string()); 44 self.addr = Some(addr.to_string());
58 match connection::connect(addr, &self.conn) { 45 match connection::connect(addr, &self.conn) {
59 Ok(s) => { 46 Ok((s, authed_user)) => {
60 self.stream = Some(s); 47 self.stream = Some(s);
48 self.logged_in_user = authed_user;
61 database::save_host(&self.conn, addr); 49 database::save_host(&self.conn, addr);
62 } 50 }
63 Err(e) => println!("Failed to connect: {}", e), 51 Err(e) => println!("Failed to connect: {}", e),
@@ -142,6 +130,7 @@ impl ClientState {
142 } 130 }
143 "disconnect" => { 131 "disconnect" => {
144 self.stream = None; 132 self.stream = None;
133 self.logged_in_user = None;
145 println!("\nDisconnected"); 134 println!("\nDisconnected");
146 } 135 }
147 "ping" => { 136 "ping" => {
@@ -165,7 +154,7 @@ impl ClientState {
165 self.stream = None; 154 self.stream = None;
166 println!("\nConnection lost"); 155 println!("\nConnection lost");
167 } else { 156 } else {
168 if response.trim() == "PING" { 157 if response.trim() == "PONG" {
169 let elapsed = start.elapsed(); 158 let elapsed = start.elapsed();
170 println!("\nPong ({:?})", elapsed); 159 println!("\nPong ({:?})", elapsed);
171 } else { 160 } else {
@@ -187,7 +176,7 @@ impl ClientState {
187 if password.is_empty() { 176 if password.is_empty() {
188 continue 'outer; 177 continue 'outer;
189 } 178 }
190 let hashed = hash_password(&password); 179 let hashed = format!("{:x}", Sha256::digest(password.as_bytes()));
191 let msg = format!("REGISTER {} {}", username, hashed); 180 let msg = format!("REGISTER {} {}", username, hashed);
192 let s = self.stream.as_mut().unwrap(); 181 let s = self.stream.as_mut().unwrap();
193 let writer_stream = s.try_clone().unwrap(); 182 let writer_stream = s.try_clone().unwrap();
@@ -204,7 +193,14 @@ impl ClientState {
204 self.stream = None; 193 self.stream = None;
205 println!("\nConnection lost"); 194 println!("\nConnection lost");
206 } else { 195 } else {
207 println!("\n{}", response.trim()); 196 let resp = response.trim();
197 if resp == format!("REGISTERED {}", username) {
198 println!("\nSuccessfully registered as {}", username);
199 } else if resp.starts_with("ERROR ") {
200 println!("\nRegistration failed: {}", &resp[6..]);
201 } else {
202 println!("\n{}", resp);
203 }
208 } 204 }
209 } 205 }
210 "login" => { 206 "login" => {
@@ -212,6 +208,10 @@ impl ClientState {
212 println!("\nNot connected. Use 'connect <ip>:<port>' first."); 208 println!("\nNot connected. Use 'connect <ip>:<port>' first.");
213 continue 'outer; 209 continue 'outer;
214 } 210 }
211 if self.logged_in_user.is_some() {
212 println!("\nAlready logged in as {}", self.logged_in_user.as_ref().unwrap());
213 continue 'outer;
214 }
215 loop { 215 loop {
216 let username = read_line("\nUsername: "); 216 let username = read_line("\nUsername: ");
217 if username.is_empty() { 217 if username.is_empty() {
@@ -221,7 +221,7 @@ impl ClientState {
221 if password.is_empty() { 221 if password.is_empty() {
222 break; 222 break;
223 } 223 }
224 let hashed = hash_password(&password); 224 let hashed = format!("{:x}", Sha256::digest(password.as_bytes()));
225 let msg = format!("AUTH {} {}", username, hashed); 225 let msg = format!("AUTH {} {}", username, hashed);
226 let s = self.stream.as_mut().unwrap(); 226 let s = self.stream.as_mut().unwrap();
227 let writer_stream = s.try_clone().unwrap(); 227 let writer_stream = s.try_clone().unwrap();
@@ -240,17 +240,21 @@ impl ClientState {
240 break; 240 break;
241 } else { 241 } else {
242 let resp = response.trim(); 242 let resp = response.trim();
243 println!("\n{}", resp);
244 if resp.starts_with("OK") { 243 if resp.starts_with("OK") {
245 // Save credentials 244 // Save credentials
246 if let Some(addr) = &self.addr { 245 if let Some(addr) = &self.addr {
247 if let Err(e) = database::save_credentials(&self.conn, addr, &username, &password) { 246 if let Err(e) = database::save_credentials(&self.conn, addr, &username, &hashed) {
248 println!("Failed to save credentials: {}", e); 247 println!("Failed to save credentials: {}", e);
249 } 248 }
250 } 249 }
250 self.logged_in_user = Some(username.clone());
251 println!("\nLogged in as {}", username);
251 break; 252 break;
253 } else if resp.starts_with("ERROR ") {
254 println!("\nAuthentication failed: {}", &resp[6..]);
255 println!("Try again or press Enter to cancel.");
252 } else { 256 } else {
253 println!("Authentication failed, try again or press Enter to cancel."); 257 println!("\n{}", resp);
254 } 258 }
255 } 259 }
256 } 260 }
@@ -268,14 +272,54 @@ impl ClientState {
268 if password.is_empty() { 272 if password.is_empty() {
269 continue 'outer; 273 continue 'outer;
270 } 274 }
275 let hashed = format!("{:x}", Sha256::digest(password.as_bytes()));
271 if let Some(addr) = &self.addr { 276 if let Some(addr) = &self.addr {
272 if let Err(e) = database::save_credentials(&self.conn, addr, &username, &password) { 277 if let Err(e) = database::save_credentials(&self.conn, addr, &username, &hashed) {
273 println!("Failed to save credentials: {}", e); 278 println!("Failed to save credentials: {}", e);
274 } else { 279 } else {
275 println!("Credentials saved for {}", addr); 280 println!("Credentials saved for {}", addr);
276 } 281 }
277 } 282 }
278 } 283 }
284 "logout" => {
285 self.logged_in_user = None;
286 println!("\nLogged out");
287 }
288 "info" => {
289 if self.stream.is_none() {
290 println!("\nNot connected. Use 'connect <ip>:<port>' first.");
291 continue 'outer;
292 }
293 let s = self.stream.as_mut().unwrap();
294 let writer_stream = s.try_clone().unwrap();
295 let mut writer = writer_stream;
296 let response_reader_stream = s.try_clone().unwrap();
297 let mut response_reader = BufReader::new(response_reader_stream);
298 if let Err(_) = writeln!(writer, "INFO") {
299 self.stream = None;
300 println!("\nConnection lost");
301 continue 'outer;
302 }
303 let mut response = String::new();
304 if let Err(_) = response_reader.read_line(&mut response) {
305 self.stream = None;
306 println!("\nConnection lost");
307 } else {
308 let resp = response.trim();
309 if resp.starts_with("INFO ") {
310 let parts: Vec<&str> = resp.split_whitespace().collect();
311 if parts.len() == 4 {
312 println!("\nLogged in as {} (ID: {})\r\nBalance: {} PLN", parts[1], parts[2], parts[3]);
313 } else {
314 println!("\n{}", resp);
315 }
316 } else if resp.starts_with("ERROR ") {
317 println!("\nError: {}", &resp[6..]);
318 } else {
319 println!("\n{}", resp);
320 }
321 }
322 }
279 "help" => { 323 "help" => {
280 help(); 324 help();
281 } 325 }
@@ -295,11 +339,16 @@ impl ClientState {
295 disable_raw_mode().unwrap(); 339 disable_raw_mode().unwrap();
296 execute!(stdout(), Show).unwrap(); 340 execute!(stdout(), Show).unwrap();
297 return; 341 return;
342 } else if c == '\t' {
343 self.handle_tab_completion(&mut current_input);
298 } else { 344 } else {
299 current_input.push(c); 345 current_input.push(c);
300 redraw(&current_input); 346 redraw(&current_input);
301 } 347 }
302 } 348 }
349 KeyCode::Tab => {
350 self.handle_tab_completion(&mut current_input);
351 }
303 _ => {} 352 _ => {}
304 } 353 }
305 _ => {} 354 _ => {}
@@ -309,6 +358,58 @@ impl ClientState {
309 disable_raw_mode().unwrap(); 358 disable_raw_mode().unwrap();
310 execute!(stdout(), Show).unwrap(); 359 execute!(stdout(), Show).unwrap();
311 } 360 }
361
362 fn handle_tab_completion(&mut self, current_input: &mut String) {
363 let prefix = current_input.trim();
364 let commands = get_commands();
365 let matches: Vec<_> = commands.iter().filter(|c| c.starts_with(prefix)).collect();
366 if matches.len() == 1 {
367 *current_input = matches[0].clone();
368 redraw(current_input);
369 } else if matches.len() > 1 {
370 disable_raw_mode().unwrap();
371 execute!(stdout(), Show).unwrap();
372 println!("\nPossible completions:");
373 for m in &matches {
374 print!("{} ", m);
375 }
376 println!();
377 enable_raw_mode().unwrap();
378 redraw(current_input);
379 }
380 // else do nothing
381 }
382}
383
384fn get_commands() -> Vec<String> {
385 vec![
386 "connect".to_string(),
387 "send".to_string(),
388 "disconnect".to_string(),
389 "ping".to_string(),
390 "login".to_string(),
391 "register".to_string(),
392 "setcred".to_string(),
393 "logout".to_string(),
394 "info".to_string(),
395 "help".to_string(),
396 "quit".to_string(),
397 ]
398}
399
400fn help() {
401 println!("\nAvailable commands:\r
402 connect <ip>:<port> - connect to the server\r
403 send <message> - send a message to the server\r
404 disconnect - disconnect from the server\r
405 ping - ping the server\r
406 login - login to the server\r
407 register - register a new user\r
408 setcred - set credentials for a host\r
409 logout - logout from the server\r
410 info - show login info\r
411 help - show this message\r
412 quit - quit the program\n");
312} 413}
313 414
314fn redraw(current_input: &str) { 415fn redraw(current_input: &str) {
@@ -326,10 +427,5 @@ fn read_line(prompt: &str) -> String {
326 let mut input = String::new(); 427 let mut input = String::new();
327 std::io::stdin().read_line(&mut input).unwrap(); 428 std::io::stdin().read_line(&mut input).unwrap();
328 enable_raw_mode().unwrap(); 429 enable_raw_mode().unwrap();
329 execute!(stdout(), crossterm::cursor::Hide).unwrap();
330 input.trim().to_string() 430 input.trim().to_string()
331} 431}
332
333fn help() {
334 println!("Available commands: connect <ip>:<port>, send <message>, disconnect, ping, login, register, setcred, help, quit");
335}