diff --git a/AUTHENTICATION.md b/AUTHENTICATION.md new file mode 100644 index 0000000..e6bbd74 --- /dev/null +++ b/AUTHENTICATION.md @@ -0,0 +1,276 @@ +# SCAR Chat - Authentication & Nickname System + +## Overview + +The SCAR Chat system now includes user authentication with database-backed nicknames. Each client must login with valid credentials before they can send messages or perform actions. + +## Authentication Flow + +### Server-Side + +1. **Client connects** via SSL/TLS +2. **Server waits for LOGIN message**: `LOGIN:username:password` +3. **Server validates credentials** against SQLite database +4. **If valid**: Server sends `LOGIN_SUCCESS:username` and accepts messages from this client +5. **If invalid**: Server sends `LOGIN_FAILED:reason` and blocks messages +6. **All messages must come from authenticated clients** - unauthenticated clients are rejected with `ERROR:Not authenticated` + +### Client-Side (Android) + +1. User enters server hostname and port +2. Click "Connect" to establish SSL connection +3. After connecting, username/password fields become enabled +4. User enters credentials and clicks "Login" +5. On successful login, credentials fields are disabled and user can send messages +6. All sent messages show the authenticated username in the format: `*{HH:MM:SS}* username: message` + +## Message Format + +All messages now include timestamp and authenticated username: + +``` +*{HH:MM:SS}* username: message content +``` + +**Examples:** +- `*{14:32:45}* alice: Hello everyone!` +- `*{14:32:52}* admin: System message` +- `*{14:33:01}* bob: Nice to meet you` + +## Server Authentication Flow + +### 1. Parse LOGIN Message + +```cpp +LOGIN:username:password +``` + +Server extracts username and password from the message format. + +### 2. Database Lookup + +```cpp +if (global_db->authenticate_user(username, password)) { + // Authentication successful + nickname = username; + authenticated = true; + client_nicknames[ssl] = username; +} +``` + +The database validates the credentials by: +- Checking if user exists +- Checking if user account is active +- Computing SHA256(password + salt) and comparing with stored hash + +### 3. Track Authenticated Client + +```cpp +std::map client_nicknames; // Track authenticated clients +std::mutex nicknames_mutex; +``` + +The server maintains a map of SSL connections to usernames to ensure: +- Only authenticated clients can send messages +- Camera events are attributed to correct user +- Disconnects are properly logged + +### 4. Accept/Reject Messages + +**Authenticated clients:** +- Messages are accepted and broadcast with nickname +- Format: `*{timestamp}* nickname: message` +- CAMERA_ENABLE/DISABLE events also use nickname + +**Unauthenticated clients:** +- Messages are rejected +- Response: `ERROR:Not authenticated. Send LOGIN:username:password` +- Client must send LOGIN message first + +## Server API + +### Initialize with Database + +```cpp +// In main() +global_db = new Database("scar_chat.db"); +if (!global_db->initialize()) { + std::cerr << "Failed to initialize database" << std::endl; + return 1; +} +``` + +### Per-Client Handler + +```cpp +void handle_client(SSL *ssl, int client_socket) { + std::string nickname; + bool authenticated = false; + + // Wait for LOGIN message + // Validate against database + // If valid, set authenticated = true and nickname = username + + // Reject all messages from unauthenticated clients + if (!authenticated) { + SSL_write(ssl, error_msg); + continue; + } + + // Process authenticated messages +} +``` + +## Android Client Flow + +### UI Components Added + +- **Username Input**: Text field (disabled until connected) +- **Password Input**: Password field (disabled until connected) +- **Login Button**: Sends LOGIN:username:password (disabled until connected) +- **Connection Status**: Shows current connection and login state + +### Login Handling + +```java +private void attemptLogin() { + String username = usernameInput.getText().toString().trim(); + String password = passwordInput.getText().toString().trim(); + + String loginCmd = "LOGIN:" + username + ":" + password; + chatConnection.sendMessage(loginCmd); +} +``` + +### Response Handling + +```java +@Override +public void onMessageReceived(String message) { + if (message.startsWith("LOGIN_SUCCESS:")) { + // Extract username + currentUsername = message.substring("LOGIN_SUCCESS:".length()).trim(); + // Enable chat + connectionStatus.setText("Logged in as: " + currentUsername); + loginBtn.setEnabled(false); + } else if (message.startsWith("LOGIN_FAILED:")) { + // Show error + String reason = message.substring("LOGIN_FAILED:".length()).trim(); + Toast.makeText(this, "Login failed: " + reason, Toast.LENGTH_SHORT).show(); + } +} +``` + +### Send Messages with Nickname + +```java +private void sendMessage() { + if (mainActivity.getCurrentUsername().isEmpty()) { + Toast.makeText(getContext(), "Please login first", Toast.LENGTH_SHORT).show(); + return; + } + + String username = mainActivity.getCurrentUsername(); + String timestamp = new SimpleDateFormat("HH:mm:ss").format(new Date()); + String formattedMsg = "*{" + timestamp + "}* " + username + ": " + message; +} +``` + +## Database User Management + +### Create Users for Testing + +```bash +cd build +./dbmanager register alice AlicePassword789 alice@scar.local user +./dbmanager register bob BobPassword000 bob@scar.local user +./dbmanager register admin AdminPass123 admin@scar.local admin +``` + +### Verify Users + +```bash +./dbmanager list +``` + +### Test Authentication + +```bash +./dbmanager authenticate alice AlicePassword789 +``` + +## Running the System + +### 1. Setup Database + +```bash +cd build +./dbmanager register alice AlicePassword789 alice@scar.local user +./dbmanager register bob BobPassword000 bob@scar.local user +``` + +### 2. Start Server + +```bash +./chat_server ../certs/server.crt ../certs/server.key +``` + +Server output: +``` +Database initialized successfully +SCAR Chat Server listening on port 42317 +``` + +### 3. Connect Android Client + +1. Open SCAR Chat app +2. Enter server hostname (e.g., "192.168.1.100") +3. Enter port (42317) +4. Click "Connect" +5. Username/Password fields become enabled +6. Enter credentials: alice / AlicePassword789 +7. Click "Login" +8. Send messages - they will appear with nickname "alice" + +### 4. Verify Server Logs + +``` +User authenticated: alice (FD: 4) +User authenticated: bob (FD: 5) +Received: *{14:32:45}* alice: Hello Bob! +``` + +## Security Considerations + +1. **Passwords are NOT transmitted in plain text** - TLS/SSL encrypts all traffic +2. **Passwords are stored securely** - SHA256 with unique salt per user +3. **Each connection is independent** - disconnecting doesn't affect other users +4. **Unauthenticated clients are isolated** - cannot access chat or perform actions + +## Error Handling + +### Server-Side Errors + +| Message | Meaning | +|---------|---------| +| `LOGIN_FAILED:Invalid credentials` | Username not found or password incorrect | +| `ERROR:Not authenticated` | Client tried to send message without logging in | +| `ERROR:Account inactive` | User account has been deactivated | + +### Client-Side Errors + +| Error | Resolution | +|-------|-----------| +| "Not connected to server" | Click Connect first | +| "Login failed: Invalid credentials" | Check username/password spelling | +| "Please login first" | User must complete login before sending messages | + +## Future Enhancements + +- Session tokens with expiration +- Multiple devices per user +- Rate limiting per user +- Activity logging +- User profile information +- Message history per user +- Direct messaging between users diff --git a/android_client/app/src/main/AndroidManifest.xml b/android_client/app/src/main/AndroidManifest.xml index 768e3af..aff5277 100644 --- a/android_client/app/src/main/AndroidManifest.xml +++ b/android_client/app/src/main/AndroidManifest.xml @@ -17,7 +17,7 @@ android:theme="@style/Theme.SCARChat"> @@ -25,6 +25,10 @@ + + diff --git a/android_client/app/src/main/java/com/scar/chat/ChatConnection.java b/android_client/app/src/main/java/com/scar/chat/ChatConnection.java index fb117e6..6d65509 100644 --- a/android_client/app/src/main/java/com/scar/chat/ChatConnection.java +++ b/android_client/app/src/main/java/com/scar/chat/ChatConnection.java @@ -54,13 +54,17 @@ public class ChatConnection { in = new BufferedReader(new InputStreamReader(socket.getInputStream())); connected = true; - listener.onConnected(); + if (listener != null) { + listener.onConnected(); + } // Start reading messages readMessages(); } catch (Exception e) { connected = false; - listener.onError("Connection failed: " + e.getMessage()); + if (listener != null) { + listener.onError("Connection failed: " + e.getMessage()); + } } }).start(); } @@ -70,10 +74,12 @@ public class ChatConnection { try { String message; while (connected && (message = in.readLine()) != null) { - listener.onMessageReceived(message); + if (listener != null) { + listener.onMessageReceived(message); + } } } catch (IOException e) { - if (connected) { + if (connected && listener != null) { listener.onError("Read error: " + e.getMessage()); } } finally { @@ -92,21 +98,28 @@ public class ChatConnection { out.flush(); } } catch (Exception e) { - listener.onError("Failed to send message: " + e.getMessage()); + if (listener != null) { + listener.onError("Failed to send message: " + e.getMessage()); + } } }).start(); } public void disconnect() { - connected = false; - try { - if (out != null) out.close(); - if (in != null) in.close(); - if (socket != null && !socket.isClosed()) socket.close(); - } catch (IOException e) { - e.printStackTrace(); - } - listener.onDisconnected(); + // Run on background thread to avoid NetworkOnMainThreadException + new Thread(() -> { + connected = false; + try { + if (out != null) out.close(); + if (in != null) in.close(); + if (socket != null && !socket.isClosed()) socket.close(); + } catch (IOException e) { + e.printStackTrace(); + } + if (listener != null) { + listener.onDisconnected(); + } + }).start(); } public boolean isConnected() { diff --git a/android_client/app/src/main/java/com/scar/chat/ChatFragment.java b/android_client/app/src/main/java/com/scar/chat/ChatFragment.java index e51eb5f..0faa067 100644 --- a/android_client/app/src/main/java/com/scar/chat/ChatFragment.java +++ b/android_client/app/src/main/java/com/scar/chat/ChatFragment.java @@ -6,13 +6,17 @@ import android.view.View; import android.view.ViewGroup; import android.widget.*; import androidx.fragment.app.Fragment; +import java.text.SimpleDateFormat; +import java.util.Date; +import java.util.Locale; public class ChatFragment extends Fragment { - private TextSwitcher chatDisplay; + private TextView chatDisplay; private EditText messageInput; private Button sendBtn, bgColorBtn, textColorBtn; private SeekBar transparencySlider; private TextView transparencyValue; + private ScrollView chatScroll; private MainActivity mainActivity; @Override @@ -30,6 +34,10 @@ public class ChatFragment extends Fragment { textColorBtn = root.findViewById(R.id.text_color_btn); transparencySlider = root.findViewById(R.id.transparency_slider); transparencyValue = root.findViewById(R.id.transparency_value); + chatScroll = root.findViewById(R.id.chat_scroll); + + // Make chat display read-only and scrollable + chatDisplay.setMovementMethod(new android.text.method.ScrollingMovementMethod()); // Send button sendBtn.setOnClickListener(v -> sendMessage()); @@ -61,25 +69,59 @@ public class ChatFragment extends Fragment { private void sendMessage() { String message = messageInput.getText().toString().trim(); if (!message.isEmpty()) { - if (mainActivity != null && mainActivity.getChatConnection().isConnected()) { - try { - mainActivity.getChatConnection().sendMessage(message); - addMessage("[You] " + message); - messageInput.setText(""); - } catch (Exception e) { - Toast.makeText(getContext(), "Error sending message: " + e.getMessage(), Toast.LENGTH_SHORT).show(); - } - } else { - Toast.makeText(getContext(), "Not connected", Toast.LENGTH_SHORT).show(); + if (mainActivity == null) { + Toast.makeText(getContext(), "Activity not initialized", Toast.LENGTH_SHORT).show(); + return; + } + + ChatConnection conn = mainActivity.getChatConnection(); + if (conn == null) { + Toast.makeText(getContext(), "Connection not initialized", Toast.LENGTH_SHORT).show(); + return; + } + + if (!conn.isConnected()) { + Toast.makeText(getContext(), "Not connected to server", Toast.LENGTH_SHORT).show(); + return; + } + + String username = mainActivity.getCurrentUsername(); + if (username == null || username.isEmpty()) { + Toast.makeText(getContext(), "Please login first", Toast.LENGTH_SHORT).show(); + return; + } + + try { + conn.sendMessage(message); + // Format message with timestamp and current username + String timestamp = new SimpleDateFormat("HH:mm:ss", Locale.US).format(new Date()); + String formattedMsg = "*{" + timestamp + "}* " + username + ": " + message; + addMessage(formattedMsg); + messageInput.setText(""); + } catch (Exception e) { + Toast.makeText(getContext(), "Error sending message: " + e.getMessage(), Toast.LENGTH_SHORT).show(); } } } public void addMessage(String message) { - if (chatDisplay != null) { - // Simple text view update (for demo) - // In production, use RecyclerView for better performance - chatDisplay.setText(message); + try { + if (chatDisplay != null) { + // Append message to existing text + String currentText = chatDisplay.getText().toString(); + if (currentText.isEmpty()) { + chatDisplay.setText(message); + } else { + chatDisplay.append("\n" + message); + } + + // Auto-scroll to bottom + if (chatScroll != null) { + chatScroll.post(() -> chatScroll.fullScroll(View.FOCUS_DOWN)); + } + } + } catch (Exception e) { + e.printStackTrace(); } } diff --git a/android_client/app/src/main/java/com/scar/chat/LoginActivity.java b/android_client/app/src/main/java/com/scar/chat/LoginActivity.java new file mode 100644 index 0000000..e19655b --- /dev/null +++ b/android_client/app/src/main/java/com/scar/chat/LoginActivity.java @@ -0,0 +1,131 @@ +package com.scar.chat; + +import android.content.Intent; +import android.os.Bundle; +import android.widget.Button; +import android.widget.EditText; +import android.widget.TextView; +import android.widget.Toast; +import androidx.appcompat.app.AppCompatActivity; + +public class LoginActivity extends AppCompatActivity { + private EditText serverInput, portInput, usernameInput, passwordInput; + private Button loginButton; + private TextView infoText; + private ChatConnection chatConnection; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + setContentView(R.layout.activity_login); + + // Initialize UI components + serverInput = findViewById(R.id.server_input); + portInput = findViewById(R.id.port_input); + usernameInput = findViewById(R.id.username_input); + passwordInput = findViewById(R.id.password_input); + loginButton = findViewById(R.id.login_button); + infoText = findViewById(R.id.info_text); + + // Set default values + serverInput.setText("localhost"); + portInput.setText("42317"); + + // Initialize chat connection with callback + chatConnection = new ChatConnection(new ChatConnection.ConnectionListener() { + @Override + public void onConnected() { + runOnUiThread(() -> { + infoText.setText("Connected to server. Sending credentials..."); + sendLogin(); + }); + } + + @Override + public void onDisconnected() { + runOnUiThread(() -> { + infoText.setText("Disconnected"); + loginButton.setEnabled(true); + }); + } + + @Override + public void onMessageReceived(String message) { + runOnUiThread(() -> { + if (message.startsWith("LOGIN_SUCCESS:")) { + String username = message.substring("LOGIN_SUCCESS:".length()).trim(); + Toast.makeText(LoginActivity.this, "Login successful!", Toast.LENGTH_SHORT).show(); + + // Start MainActivity and pass the username + Intent intent = new Intent(LoginActivity.this, MainActivity.class); + intent.putExtra("username", username); + intent.putExtra("server", serverInput.getText().toString()); + intent.putExtra("port", portInput.getText().toString()); + startActivity(intent); + finish(); + } else if (message.startsWith("LOGIN_FAILED:")) { + String reason = message.substring("LOGIN_FAILED:".length()).trim(); + infoText.setText("Login failed: " + reason); + chatConnection.disconnect(); + loginButton.setEnabled(true); + } + }); + } + + @Override + public void onError(String error) { + runOnUiThread(() -> { + infoText.setText("Error: " + error); + chatConnection.disconnect(); + loginButton.setEnabled(true); + }); + } + }); + + // Login button click listener + loginButton.setOnClickListener(v -> onLoginClick()); + } + + private void onLoginClick() { + String server = serverInput.getText().toString().trim(); + String portStr = portInput.getText().toString().trim(); + String username = usernameInput.getText().toString().trim(); + String password = passwordInput.getText().toString().trim(); + + // Validate inputs + if (server.isEmpty() || portStr.isEmpty() || username.isEmpty() || password.isEmpty()) { + Toast.makeText(this, "Please fill in all fields", Toast.LENGTH_SHORT).show(); + return; + } + + try { + int port = Integer.parseInt(portStr); + + // Disable button during login + loginButton.setEnabled(false); + infoText.setText("Connecting to server..."); + + // Connect to server + chatConnection.connect(server, port); + } catch (NumberFormatException e) { + Toast.makeText(this, "Invalid port number", Toast.LENGTH_SHORT).show(); + loginButton.setEnabled(true); + } + } + + private void sendLogin() { + String username = usernameInput.getText().toString().trim(); + String password = passwordInput.getText().toString().trim(); + String loginCmd = "LOGIN:" + username + ":" + password; + chatConnection.sendMessage(loginCmd); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + // Disconnect on background thread to avoid NetworkOnMainThreadException + if (chatConnection != null && chatConnection.isConnected()) { + new Thread(chatConnection::disconnect).start(); + } + } +} diff --git a/android_client/app/src/main/java/com/scar/chat/MainActivity.java b/android_client/app/src/main/java/com/scar/chat/MainActivity.java index e2c1792..1eade3d 100644 --- a/android_client/app/src/main/java/com/scar/chat/MainActivity.java +++ b/android_client/app/src/main/java/com/scar/chat/MainActivity.java @@ -19,14 +19,16 @@ import androidx.fragment.app.Fragment; import com.google.android.material.tabs.TabLayout; public class MainActivity extends AppCompatActivity implements ChatConnection.ConnectionListener { - private EditText hostInput, portInput; - private Button connectBtn; private ChatConnection chatConnection; private ViewPager2 tabPager; private TabAdapter tabAdapter; private TabLayout tabLayout; + private TextView connectionStatus; private int currentBgColor = Color.WHITE; private int currentTextColor = Color.BLACK; + private String currentUsername = ""; + private String serverAddress = ""; + private int serverPort = 0; private static final int PERMISSION_REQUEST_CODE = 100; @@ -35,13 +37,33 @@ public class MainActivity extends AppCompatActivity implements ChatConnection.Co super.onCreate(savedInstanceState); setContentView(R.layout.activity_main); + // Get authenticated username and server info from LoginActivity + currentUsername = getIntent().getStringExtra("username"); + serverAddress = getIntent().getStringExtra("server"); + String portStr = getIntent().getStringExtra("port"); + + // Handle null values safely + if (currentUsername == null || currentUsername.isEmpty()) { + currentUsername = "Unknown"; + } + if (serverAddress == null || serverAddress.isEmpty()) { + serverAddress = "localhost"; + } + if (portStr == null || portStr.isEmpty()) { + serverPort = 42317; + } else { + try { + serverPort = Integer.parseInt(portStr); + } catch (NumberFormatException e) { + serverPort = 42317; + } + } + // Request permissions requestPermissions(); // Initialize UI components - hostInput = findViewById(R.id.host_input); - portInput = findViewById(R.id.port_input); - connectBtn = findViewById(R.id.connect_btn); + connectionStatus = findViewById(R.id.connection_status); tabPager = findViewById(R.id.tab_pager); tabLayout = findViewById(R.id.tab_layout); @@ -60,15 +82,13 @@ public class MainActivity extends AppCompatActivity implements ChatConnection.Co } }).attach(); - // Set default values - hostInput.setText("localhost"); - portInput.setText("42317"); + connectionStatus.setText("Logged in as: " + currentUsername); - // Connect button listener - connectBtn.setOnClickListener(v -> toggleConnection()); - - // Initialize chat connection - chatConnection = new ChatConnection(this); + // Initialize chat connection after fragments are initialized + tabPager.post(() -> { + chatConnection = new ChatConnection(this); + chatConnection.connect(serverAddress, serverPort); + }); // Apply initial colors applyTheme(); @@ -89,47 +109,60 @@ public class MainActivity extends AppCompatActivity implements ChatConnection.Co } } - private void toggleConnection() { - if (chatConnection.isConnected()) { - chatConnection.disconnect(); - } else { - String host = hostInput.getText().toString(); - int port = Integer.parseInt(portInput.getText().toString()); - chatConnection.connect(host, port); - } - } - @Override public void onConnected() { runOnUiThread(() -> { - connectBtn.setText("Disconnect"); - Toast.makeText(this, "Connected!", Toast.LENGTH_SHORT).show(); + if (connectionStatus != null) { + connectionStatus.setText("Connected to server"); + } + Toast.makeText(MainActivity.this, "Connected to server", Toast.LENGTH_SHORT).show(); }); } @Override public void onDisconnected() { runOnUiThread(() -> { - connectBtn.setText("Connect"); - Toast.makeText(this, "Disconnected", Toast.LENGTH_SHORT).show(); + connectionStatus.setText("Disconnected"); + Toast.makeText(MainActivity.this, "Disconnected from server", Toast.LENGTH_SHORT).show(); }); } @Override public void onMessageReceived(String message) { runOnUiThread(() -> { - if (tabAdapter != null) { - ChatFragment chatFrag = tabAdapter.getChatFragment(); - if (chatFrag != null) { - chatFrag.addMessage(message); + try { + // Handle error messages + if (message.startsWith("ERROR:")) { + String error = message.substring("ERROR:".length()).trim(); + Toast.makeText(MainActivity.this, "Error: " + error, Toast.LENGTH_SHORT).show(); + } else { + // Regular chat message - display in chat fragment + if (tabPager != null && tabPager.getAdapter() != null) { + TabAdapter adapter = (TabAdapter) tabPager.getAdapter(); + Fragment fragment = adapter.getFragment(0); + if (fragment instanceof ChatFragment && fragment.isAdded()) { + ((ChatFragment) fragment).addMessage(message); + } + } } + } catch (Exception e) { + e.printStackTrace(); } }); } @Override public void onError(String error) { - runOnUiThread(() -> Toast.makeText(this, error, Toast.LENGTH_LONG).show()); + runOnUiThread(() -> { + if (connectionStatus != null) { + connectionStatus.setText("Error: " + error); + } + Toast.makeText(MainActivity.this, "Connection error: " + error, Toast.LENGTH_SHORT).show(); + }); + } + + public String getCurrentUsername() { + return currentUsername; } private void applyTheme() { @@ -177,5 +210,16 @@ public class MainActivity extends AppCompatActivity implements ChatConnection.Co ChatFragment getChatFragment() { return chatFragment; } + + Fragment getFragment(int position) { + switch (position) { + case 0: + return chatFragment; + case 1: + return videoFragment; + default: + return null; + } + } } } diff --git a/android_client/app/src/main/res/layout/activity_login.xml b/android_client/app/src/main/res/layout/activity_login.xml new file mode 100644 index 0000000..2b4c267 --- /dev/null +++ b/android_client/app/src/main/res/layout/activity_login.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + +