Skip to main content

Client & Auth Layers

The com.dfs.client and com.dfs.auth packages handle the user-facing side of the system.


Authentication Service

AuthService

Implements AuthServiceInterface and extends UnicastRemoteObject. Uses two ConcurrentHashMap stores:

public class AuthService extends UnicastRemoteObject implements AuthServiceInterface {
// username → UserRecord (salt, hashed password)
private final ConcurrentHashMap<String, UserRecord> users = new ConcurrentHashMap<>();

// token → username
private final ConcurrentHashMap<String, String> tokens = new ConcurrentHashMap<>();
}

Registration flow:

public String register(UserCredentials creds) throws RemoteException {
String username = creds.getUsername();
String password = creds.getPassword();

// Validate inputs
if (username == null || username.isBlank()) return "ERROR: Username required";
if (password == null || password.length() < 6) return "ERROR: Password must be at least 6 characters";

// Hash with random salt
String salt = PasswordUtils.generateSalt();
String hash = PasswordUtils.hashPassword(password, salt);

UserRecord record = new UserRecord(salt, hash);
UserRecord existing = users.putIfAbsent(username, record);
if (existing != null) return "ERROR: Username already taken";

// Auto-login: generate token immediately
String token = UUID.randomUUID().toString();
tokens.put(token, username);
return token;
}

Login flow:

public String login(UserCredentials creds) throws RemoteException {
UserRecord record = users.get(creds.getUsername());
if (record == null) return "ERROR: User not found";

if (!PasswordUtils.verify(creds.getPassword(), record.salt(), record.hash())) {
return "ERROR: Invalid password";
}

String token = UUID.randomUUID().toString();
tokens.put(token, creds.getUsername());
return token;
}

PasswordUtils

Four critical methods implemented using only the JDK:

public class PasswordUtils {
private static final int SALT_BYTES = 16;
private static final int HASH_ITERATIONS = 260_000; // OWASP 2023 minimum
private static final int KEY_LENGTH = 256;

// Generates 16 cryptographically random bytes, Base64-encoded
public static String generateSalt() { ... }

// PBKDF2WithHmacSHA256(password, salt, 260000, 256) → Base64
public static String hashPassword(String password, String salt) { ... }

// Re-derives hash and compares with MessageDigest.isEqual (constant-time)
public static boolean verify(String password, String salt, String storedHash) { ... }
}

Why MessageDigest.isEqual and not String.equals?
String.equals returns false as soon as it finds a byte that doesn't match. An attacker can measure response times to determine how many bytes of the hash they got correct, then brute-force byte by byte. MessageDigest.isEqual always processes the entire input, eliminating this timing side-channel.


Client Layer

DFSClient

Handles all RMI connections and remote calls:

public class DFSClient {
private AuthServiceInterface authService;
private ReplicaNodeInterface connectedNode;
private String sessionToken;
private String username;

// Connect to a node over mTLS
public void connect(String host, int port) throws RemoteException {
Registry registry = LocateRegistry.getRegistry(host, port, new SslRMIClientSocketFactory());
connectedNode = (ReplicaNodeInterface) registry.lookup("ReplicaNode" + (port - 1099));
}

// Connect to auth service
public void connectAuth() throws RemoteException {
Registry registry = LocateRegistry.getRegistry("localhost", 1098, new SslRMIClientSocketFactory());
authService = (AuthServiceInterface) registry.lookup("AuthService");
}
}

ClientShell

The interactive CLI. A state machine with two states:

[NOT LOGGED IN] [LOGGED IN]
1. Register 1. Upload file
2. Login 2. Download file
3. Exit 3. Delete file
4. Rename file
5. Search files
6. List all files
7. Logout
8. Exit

Write operations generate fresh nonce and timestamp:

private void doUpload() throws RemoteException {
System.out.print("Local file path: ");
String localPath = scanner.nextLine();
System.out.print("Remote filename: ");
String remoteName = scanner.nextLine();

byte[] data = Files.readAllBytes(Paths.get(localPath));
FileOperation op = FileOperation.upload(username, remoteName, data);
// nonce and timestamp automatically populated by the factory

OperationResult result = client.handleClientOperation(op, sessionToken);
System.out.println(result.isSuccess() ? "[OK] Uploaded" : "[FAIL] " + result.getMessage());
}

Vulnerable Variants

VulnerableAuthService

Key differences from the secure version:

  • super(0) — no SSL socket factory (plain TCP)
  • Passwords stored as plaintext (no PBKDF2 hashing)
  • No constant-time comparison (String.equals)

VulnerableDFSClient

Key differences:

  • Connects to plain TCP registry (no SslRMIClientSocketFactory)
  • No nonce/timestamp generation on write operations
  • Session token sent but never validated by the server anyway

VulnerableClientShell

  • Identical menu structure to the secure version
  • No nonce/timestamp generated on operations
  • Prints a warning: [VulnClient] WARNING: Connecting WITHOUT TLS

The Complete Startup Sequence

1. AuthMain.main()
├── Creates AuthService with mTLS socket factory
├── Creates registry on port 1098 with SSL factories
└── Binds "AuthService"

2. NodeMain.main() (one instance per node)
├── Sets java.rmi.server.useCodebaseOnly = true
├── Configures TLS keystore and truststore
├── Creates ReplicaNode with SSL factories
├── Creates registry on port 1099+X
├── Binds "ReplicaNodeX"
├── Connects to AuthService via RMI
├── Discovers peers via NodeRegistry
└── Starts Raft election timer

3. ClientMain.main()
├── Configures TLS keystore and truststore
├── Connects to AuthService (port 1098)
├── Connects to a ReplicaNode (port 1099)
└── Starts ClientShell interactive loop

Next: → Security Analysis — Vulnerability 1