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