import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.*;
import java.security.*;
import java.util.*;
public class FileSystem {
private static final Path SALT_PATH = Paths.get("salt.txt");
private static final Path SHADOW_PATH = Paths.get("shadow.txt");
private static final Path STORE_PATH = Paths.get("Files.store");
private static class VFile {
String name;
String owner;
int level; // classification
StringBuilder data; // content
VFile(String name, String owner, int level) {
this.name = name;
this.owner = owner;
this.level = level;
this.data = new StringBuilder();
}
String serialize() {
// name|owner|level|content (content escaped as base64 to remain readable/safe)
String contentB64 = Base64.getEncoder().encodeToString(data.toString().getBytes(StandardCharsets.UTF_8));
return name + "|" + owner + "|" + level + "|" + contentB64;
}
static VFile deserialize(String line) {
String[] parts = line.split("\\|", 4);
if (parts.length < 4) return null;
VFile f = new VFile(parts[0], parts[1], Integer.parseInt(parts[2]));
try {
byte[] bytes = Base64.getDecoder().decode(parts[3]);
f.data.append(new String(bytes, StandardCharsets.UTF_8));
} catch (IllegalArgumentException e) {
// ignore decode errors; leave content empty
}
return f;
}
}
private final Map<String, VFile> files = new LinkedHashMap<>(); // preserve insertion order
private static class Session {
String username;
int clearance;
}
public static void main(String[] args) throws Exception {
// Always print MD5 test first
String testInput = "This is a test";
System.out.println("MD5 (\"" + testInput + "\") = " + md5Hex(testInput));
FileSystem app = new FileSystem();
if (args.length == 1 && "-i".equals(args[0])) {
app.initialiseUser();
return;
}
app.loadStoreIfPresent();
Session s = app.loginFlow();
if (s != null) {
app.menuLoop(s);
}
}
// UserReg
private void initialiseUser() throws IOException, NoSuchAlgorithmException {
ensureFilesExist();
Scanner sc = new Scanner(System.in);
System.out.print("Username: ");
String username = sc.nextLine().trim();
if (username.isEmpty() || username.contains(":")) {
System.out.println("Invalid username.");
return;
}
Map<String, String> saltMap = readSaltFile();
if (saltMap.containsKey(username)) {
System.out.println("User already exists. Aborting.");
return;
}
String password = promptPassword(sc, "Password: ");
String confirm = promptPassword(sc, "Confirm Password: ");
if (!password.equals(confirm)) {
System.out.println("Passwords do not match. Aborting.");
return;
}
List<String> pwIssues = passwordIssues(password);
if (!pwIssues.isEmpty()) {
System.out.println("Password does not meet requirements:");
for (String issue : pwIssues) System.out.println(" - " + issue);
System.out.println("Requirements: at least 8 chars, include lowercase, uppercase, and digit.");
return;
}
int clearance = promptClearance(sc);
if (clearance < 0) return;
String salt = randomEightDigits();
String passSaltHash = md5Hex(password + salt);
// Write salt.txt line
String saltLine = username + ":" + salt + System.lineSeparator();
Files.write(SALT_PATH, saltLine.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
// Write shadow.txt line
String shadowLine = username + ":" + passSaltHash + ":" + clearance + System.lineSeparator();
Files.write(SHADOW_PATH, shadowLine.getBytes(StandardCharsets.UTF_8), StandardOpenOption.CREATE, StandardOpenOption.APPEND);
System.out.println("User '" + username + "' created with clearance " + clearance + ".");
System.out.println("Added to salt.txt and shadow.txt.");
}
//Login
private Session loginFlow() throws Exception {
ensureFilesExist();
Scanner sc = new Scanner(System.in);
System.out.print("Username: ");
String username = sc.nextLine().trim();
String password = promptPassword(sc, "Password: ");
Map<String, String> saltMap = readSaltFile();
if (!saltMap.containsKey(username)) {
System.out.println("Username not found in salt.txt. Exiting.");
return null;
}
String salt = saltMap.get(username);
System.out.println(username + " found in salt.txt");
System.out.println("salt retrieved: " + salt);
System.out.println("hashing ...");
String hash = md5Hex(password + salt);
System.out.println("hash value: " + hash);
// Files
ShadowEntry sh = readShadowFor(username);
if (sh == null) {
System.out.println("No matching entry in shadow.txt. Exiting.");
return null;
}
if (!hash.equalsIgnoreCase(sh.passSaltHash)) {
System.out.println("Authentication failed: hash mismatch.");
return null;
}
Session s = new Session();
s.username = username;
s.clearance = sh.clearance;
System.out.println("Authentication for user " + username + " complete.");
System.out.println("The clearance for " + username + " is " + s.clearance + ".");
return s;
}
private void menuLoop(Session s) throws IOException {
Scanner sc = new Scanner(System.in);
while (true) {
System.out.flush();
System.out.println(); // blank line for readability
System.out.println("Options: (C)reate, (A)ppend, (R)ead, (W)rite, (L)ist, (S)ave or (E)xit.");
System.out.print("Your choice: ");
System.out.flush();
String choice = sc.nextLine().trim();
if (choice.isEmpty()) continue;
char k = Character.toUpperCase(choice.charAt(0));
switch (k) {
case 'C':
System.out.print("Filename: ");
String newName = sc.nextLine().trim();
if (newName.isEmpty()) {
System.out.println("Invalid name.");
break;
}
if (files.containsKey(newName)) {
System.out.println("File already exists.");
} else {
VFile f = new VFile(newName, s.username, s.clearance);
files.put(newName, f);
System.out.println("File '" + newName + "' created at level " + f.level + ".");
}
break;
case 'A':
handleAppend(sc, s);
break;
case 'R':
handleRead(sc, s);
break;
case 'W':
handleWrite(sc, s, /*overwrite*/true);
break;
case 'L':
listFiles();
break;
case 'S':
saveStore();
System.out.println("Saved to " + STORE_PATH.getFileName() + ".");
break;
case 'E':
System.out.print("Shut down the FileSystem? (Y)es or (N)o ");
System.out.flush();
String yn = sc.nextLine().trim();
if (!yn.isEmpty() && Character.toUpperCase(yn.charAt(0)) == 'Y') {
System.out.println("Goodbye.");
return;
}
break;
default:
System.out.println("Unknown option.");
}
}
}
private void handleAppend(Scanner sc, Session s) {
System.out.print("Filename: ");
System.out.flush();
String name = sc.nextLine().trim();
VFile f = files.get(name);
if (f == null) { System.out.println("No such file."); return; }
if (!canWriteOrAppend(s.clearance, f.level)) {
System.out.println("Access denied (no write down). Your level: " + s.clearance + ", file level: " + f.level);
return;
}
System.out.print("Append text (single line): ");
String line = sc.nextLine();
f.data.append(line).append(System.lineSeparator());
System.out.println("Appended.");
}
private void handleWrite(Scanner sc, Session s, boolean overwrite) {
System.out.print("Filename: ");
System.out.flush();
String name = sc.nextLine().trim();
VFile f = files.get(name);
if (f == null) { System.out.println("No such file."); return; }
if (!canWriteOrAppend(s.clearance, f.level)) {
System.out.println("Access denied (no write down). Your level: " + s.clearance + ", file level: " + f.level);
return;
}
System.out.println("Enter content; end with a single line containing only " + ":wq");
StringBuilder buf = new StringBuilder();
while (true) {
String line = sc.nextLine();
if (line.equals(":wq")) break;
buf.append(line).append(System.lineSeparator());
}
if (overwrite) f.data.setLength(0);
f.data.append(buf);
System.out.println("Written.");
}
private void handleRead(Scanner sc, Session s) {
System.out.print("Filename: ");
System.out.flush();
String name = sc.nextLine().trim();
VFile f = files.get(name);
if (f == null) { System.out.println("No such file."); return; }
if (!canRead(s.clearance, f.level)) {
System.out.println("Access denied (no read up). Your level: " + s.clearance + ", file level: " + f.level);
return;
}
System.out.println("--- BEGIN " + name + " (owner=" + f.owner + ", level=" + f.level + ") ---");
System.out.print(f.data.toString());
System.out.println("--- END " + name + " ---");
}
private void listFiles() {
if (files.isEmpty()) {
System.out.println("No files in system.");
return;
}
System.out.println("Name\tOwner\tLevel\tBytes");
for (VFile f : files.values()) {
System.out.println(f.name + "\t" + f.owner + "\t" + f.level + "\t" + f.data.length());
}
}
// higher number dominates lower number.
private static boolean canRead(int subjectClearance, int objectLevel) {
return subjectClearance >= objectLevel; // no read up violation
}
private static boolean canWriteOrAppend(int subjectClearance, int objectLevel) {
return subjectClearance <= objectLevel; // no write down violation
}
// ===== Persistence for Files.store =====
private void saveStore() throws IOException {
try (BufferedWriter bw = Files.newBufferedWriter(STORE_PATH, StandardCharsets.UTF_8)) {
bw.write("# Files.store — human-readable file records\n");
bw.write("# Format: name|owner|level|base64(content)\n");
for (VFile f : files.values()) {
bw.write(f.serialize());
bw.write('\n');
}
}
}
private void loadStoreIfPresent() throws IOException {
if (!Files.exists(STORE_PATH)) return;
List<String> lines = Files.readAllLines(STORE_PATH, StandardCharsets.UTF_8);
int count = 0;
for (String line : lines) {
if (line.startsWith("#") || line.isBlank()) continue;
VFile f = VFile.deserialize(line);
if (f != null) {
files.put(f.name, f);
count++;
}
}
System.out.println("Loaded " + count + " file record(s) from " + STORE_PATH.getFileName() + ".");
}
private static class ShadowEntry {
String user;
String passSaltHash;
int clearance;
}
private static Map<String, String> readSaltFile() throws IOException {
Map<String, String> map = new HashMap<>();
if (!Files.exists(SALT_PATH)) return map;
List<String> lines = Files.readAllLines(SALT_PATH, StandardCharsets.UTF_8);
for (String line : lines) {
if (line.isBlank()) continue;
String[] parts = line.split(":", 2);
if (parts.length == 2) map.put(parts[0], parts[1]);
}
return map;
}
private static ShadowEntry readShadowFor(String username) throws IOException {
if (!Files.exists(SHADOW_PATH)) return null;
List<String> lines = Files.readAllLines(SHADOW_PATH, StandardCharsets.UTF_8);
for (String line : lines) {
if (line.isBlank()) continue;
String[] parts = line.split(":", 3);
if (parts.length == 3 && parts[0].equals(username)) {
ShadowEntry e = new ShadowEntry();
e.user = parts[0];
e.passSaltHash = parts[1];
try { e.clearance = Integer.parseInt(parts[2]); } catch (NumberFormatException ex) { return null; }
return e;
}
}
return null;
}
private static void ensureFilesExist() throws IOException {
if (!Files.exists(SALT_PATH)) Files.createFile(SALT_PATH);
if (!Files.exists(SHADOW_PATH)) Files.createFile(SHADOW_PATH);
}
private static String md5Hex(String s) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] dig = md.digest(s.getBytes(StandardCharsets.UTF_8));
StringBuilder sb = new StringBuilder();
for (byte b : dig) sb.append(String.format("%02x", b));
return sb.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
}
private static String promptPassword(Scanner sc, String prompt) {
Console console = System.console();
if (console != null) {
char[] pwd = console.readPassword(prompt);
return pwd == null ? "" : new String(pwd);
}
System.out.print(prompt);
return sc.nextLine();
}
private static List<String> passwordIssues(String pw) {
List<String> issues = new ArrayList<>();
if (pw.length() < 8) issues.add("must be at least 8 characters long");
if (!pw.chars().anyMatch(Character::isLowerCase)) issues.add("must include a lowercase letter");
if (!pw.chars().anyMatch(Character::isUpperCase)) issues.add("must include an uppercase letter");
if (!pw.chars().anyMatch(Character::isDigit)) issues.add("must include a digit");
return issues;
}
private static int promptClearance(Scanner sc) {
System.out.print("User clearance (0 or 1 or 2 or 3): ");
String in = sc.nextLine().trim();
try {
int level = Integer.parseInt(in);
if (level < 0 || level > 3) throw new NumberFormatException();
return level;
} catch (NumberFormatException e) {
System.out.println("Invalid clearance. Must be 0, 1, 2, or 3.");
return -1;
}
}
private static String randomEightDigits() {
SecureRandom rnd = new SecureRandom();
int n = rnd.nextInt(1_0000_0000); // 0..99,999,999
return String.format("%08d", n);
}
}