diff --git a/Src/Hyperion-bash/bin/help b/Src/Hyperion-bash/bin/help new file mode 100644 index 0000000..df9a33f --- /dev/null +++ b/Src/Hyperion-bash/bin/help @@ -0,0 +1,292 @@ +--:Minify:-- +-- help: display command reference with paged scrolling + +local COMMANDS = { + -- {name, usage, description, {flags...}} + -- flags: {flag, desc} + { + name = "cat", + usage = "cat [file]", + desc = "Print file contents to stdout. Reads from stdin if no file given.", + flags = {} + }, + { + name = "cd", + usage = "cd [dir]", + desc = "Change working directory. Use '-' to return to previous directory.", + flags = {} + }, + { + name = "clear", + usage = "clear", + desc = "Clear the terminal screen.", + flags = {} + }, + { + name = "echo", + usage = "echo [text...]", + desc = "Print arguments to stdout.", + flags = {} + }, + { + name = "hfetch", + usage = "hfetch", + desc = "Display system information in a neofetch-style layout.", + flags = {} + }, + { + name = "id", + usage = "id [username]", + desc = "Print user identity (uid, gid). Defaults to current user.", + flags = {} + }, + { + name = "login", + usage = "login", + desc = "System login prompt. Launched automatically at boot.", + flags = {} + }, + { + name = "ls", + usage = "ls [-alh] [dir]", + desc = "List directory contents.", + flags = { + {"-a", "Show hidden files (starting with .)"}, + {"-l", "Long format: permissions, owner, size"}, + {"-h", "Human-readable file sizes"}, + } + }, + { + name = "lsusers", + usage = "lsusers", + desc = "List all user accounts with uid, gid, home, and shell.", + flags = {} + }, + { + name = "lua", + usage = "lua", + desc = "Interactive Lua REPL prompt.", + flags = {} + }, + { + name = "mkdir", + usage = "mkdir ", + desc = "Create a directory.", + flags = {} + }, + { + name = "passwd", + usage = "passwd [username]", + desc = "Change a user password. Non-root must verify current password first.", + flags = {} + }, + { + name = "ps", + usage = "ps", + desc = "List running tasks with pid, user, name, and status.", + flags = {} + }, + { + name = "pwd", + usage = "pwd", + desc = "Print current working directory.", + flags = {} + }, + { + name = "su", + usage = "su [username]", + desc = "Switch user. Defaults to root. Root can switch without a password.", + flags = {} + }, + { + name = "sudo", + usage = "sudo [-u user] [args...]", + desc = "Run a command as another user (default root). Authenticates as current user.", + flags = { + {"-u user", "Run as the specified user (name or uid)"}, + } + }, + { + name = "sysdump", + usage = "sysdump", + desc = "List all registered kernel syscalls.", + flags = {} + }, + { + name = "useradd", + usage = "useradd [-p pw] [-g gid] [-d home] [-s shell] [-M] ", + desc = "Create a new user account.", + flags = { + {"-p pw", "Set password (prompted interactively if omitted)"}, + {"-g gid", "Set primary group id"}, + {"-d home", "Set home directory (default: /home/username)"}, + {"-s shell", "Set login shell (default: /bin/hysh)"}, + {"-M", "Do not create home directory"}, + } + }, + { + name = "userdel", + usage = "userdel [-r] ", + desc = "Delete a user account.", + flags = { + {"-r", "Also recursively remove the user's home directory"}, + } + }, + { + name = "usermod", + usage = "usermod [-l name] [-p pw] [-g gid] [-d home] [-s shell] [-L] [-U] ", + desc = "Modify an existing user account.", + flags = { + {"-l name", "Rename the user"}, + {"-p pw", "Set new password"}, + {"-g gid", "Change primary group id"}, + {"-d home", "Change home directory"}, + {"-s shell", "Change login shell"}, + {"-L", "Lock the account (disable login)"}, + {"-U", "Unlock the account"}, + } + }, + { + name = "whoami", + usage = "whoami", + desc = "Print the current username.", + flags = {} + }, + { + name = "yes", + usage = "yes [text]", + desc = "Repeatedly print 'y' (or given text) until interrupted with Ctrl+C.", + flags = {} + }, +} + +-- Build lines to display +local C_HEAD = 7 -- yellow (section headers) +local C_CMD = 5 -- cyan (command name) +local C_USAGE = 1 -- white (usage line) +local C_DESC = 13 -- grey (description) +local C_FLAG = 3 -- green (flag name) +local C_DIM = 12 -- dark grey + +-- Each entry is {text, color} +local lines = {} +local function push(text, col) lines[#lines+1] = {text, col or 1} end + +push("HyperionOS Command Reference", C_HEAD) +push(string.rep("=", 50), C_DIM) +push("", 1) + +local args = {...} +local filter = args[1] -- optional: help + +local function addCmd(cmd) + push(cmd.name, C_CMD) + push(" Usage: " .. cmd.usage, C_USAGE) + push(" " .. cmd.desc, C_DESC) + if #cmd.flags > 0 then + for _, f in ipairs(cmd.flags) do + push(" " .. f[1], C_FLAG) + push(" " .. f[2], C_DESC) + end + end + push("", 1) +end + +if filter then + local found = false + for _, cmd in ipairs(COMMANDS) do + if cmd.name == filter then addCmd(cmd); found = true; break end + end + if not found then + push("help: unknown command '" .. filter .. "'", 2) + push("Run 'help' with no arguments for the full list.", C_DESC) + end +else + push("Run 'help ' for details on a specific command.", C_DESC) + push("", 1) + for _, cmd in ipairs(COMMANDS) do addCmd(cmd) end +end + +-- Pager +local sizeStr = syscall.devctl(1, "size") +local screenW = tonumber(sizeStr:sub(1, sizeStr:find(";")-1)) or 51 +local screenH = tonumber(sizeStr:sub(sizeStr:find(";")+1)) or 19 +local pageSize = screenH - 2 -- reserve bottom row for status bar + +local scroll = 0 +local totalLines = #lines +local dirty = true + +local function render() + syscall.devctl(1, "clear") + syscall.devctl(1, "spos", 1, 1) + for row = 1, pageSize do + local li = scroll + row + if li <= totalLines then + local text, col = lines[li][1], lines[li][2] + syscall.devctl(1, "sfgc", col) + -- truncate to screen width + if #text > screenW then text = text:sub(1, screenW) end + syscall.write(1, text .. "\n") + else + syscall.write(1, "\n") + end + end + -- status bar + syscall.devctl(1, "sfgc", 16) + syscall.devctl(1, "sbgc", 13) + local pct = math.floor(math.min(100, (scroll + pageSize) / totalLines * 100)) + local status = string.format(" help -- line %d/%d (%d%%) [up/down: scroll q: quit] ", + scroll + 1, totalLines, pct) + if #status > screenW then status = status:sub(1, screenW) end + syscall.devctl(1, "spos", 1, screenH) + syscall.write(1, status .. string.rep(" ", screenW - #status)) + syscall.devctl(1, "sfgc", 1) + syscall.devctl(1, "sbgc", 16) + dirty = false +end + +-- If output fits on one screen, just print without pager +if totalLines <= pageSize then + syscall.devctl(1, "clear") + syscall.devctl(1, "spos", 1, 1) + for _, line in ipairs(lines) do + syscall.devctl(1, "sfgc", line[2]) + syscall.write(1, line[1] .. "\n") + end + syscall.devctl(1, "sfgc", 1) + return +end + +render() +while true do + local ch = syscall.read(0) + if not ch or ch == "" then + -- idle + elseif ch == "q" or ch == "Q" then + break + elseif ch == "\17" then -- up arrow + if scroll > 0 then + scroll = scroll - 1 + dirty = true + end + elseif ch == "\18" then -- down arrow + if scroll + pageSize < totalLines then + scroll = scroll + 1 + dirty = true + end + elseif ch == "\19" then -- left / page up (reuse left arrow) + scroll = math.max(0, scroll - pageSize) + dirty = true + elseif ch == "\20" then -- right / page down + scroll = math.min(totalLines - pageSize, scroll + pageSize) + dirty = true + end + if dirty then render() end +end + +-- Restore screen +syscall.devctl(1, "clear") +syscall.devctl(1, "spos", 1, 1) +syscall.devctl(1, "sfgc", 1) +syscall.devctl(1, "sbgc", 16) diff --git a/Src/Hyperion-bash/bin/hfetch b/Src/Hyperion-bash/bin/hfetch new file mode 100644 index 0000000..392282f --- /dev/null +++ b/Src/Hyperion-bash/bin/hfetch @@ -0,0 +1,104 @@ +--:Minify:-- +-- Color indices (sfgc): +-- 1=white, 2=red, 3=green, 4=blue, 5=cyan, 6=magenta, 7=yellow +-- 8=orange, 9=lime, 10=lightcyan, 11=brown, 12=darkgrey, 13=lightgrey, 14=purple, 15=chartreuse, 16=black + +local C_LOGO = 5 -- cyan +local C_WHITE = 1 -- white +local C_LABEL = 13 -- light grey (key names) +local C_SEP = 12 -- dark grey (---- separator) +local C_USER = 3 -- green (user@host) + +local function c(col) syscall.devctl(1, "sfgc", col) end + +local username = syscall.getUsername() or "Unknown" +local hostname = syscall.getHostname() or "Unknown" +local userhost = username .. "@" .. hostname + +local function formatUptime(ms) + local s = math.floor(ms / 1000) + local m = math.floor(s / 60) + local h = math.floor(m / 60) + local d = math.floor(h / 24) + s = s % 60; m = m % 60; h = h % 24 + local parts = {} + if d > 0 then parts[#parts+1] = d .. "d" end + if h > 0 then parts[#parts+1] = h .. "h" end + if m > 0 then parts[#parts+1] = m .. "m" end + parts[#parts+1] = s .. "s" + return table.concat(parts, " ") +end + +local host_str = syscall.getHost() or "Unknown" +local cc_ver = host_str:match("ComputerCraft ([%d%.]+)") or host_str + +local info = { + -- {label, value} label=nil means print value as-is (userhost / separator) + {nil, userhost}, + {nil, string.rep("-", #userhost)}, + {"OS", syscall.version() or "Unknown"}, + {"Host", cc_ver}, + {"Arch", syscall.arch() or "Unknown"}, + {"Uptime", formatUptime(syscall.getUptime() or 0)}, + {"Tasks", tostring(#(syscall.getTasks() or {}))}, + {"Shell", syscall.getEnviron("SHELL") or "Unknown"}, + {"Terminal", "TTY1"}, + {"UID", tostring(syscall.getuid())}, + {"Packages", "n/a (spm)"}, +} + +local logo = { + ".. *. .. ", + " *= +@* +* ", + " .@#. -@@@= :#@. ", + " =@@+ *@@@# +@@= ", + " %@@%: *@@@# -%@@% ", + " :@@@@+ *@@@# .*@@@@: ", + " :*@@@%- *@@@# -@@@@*: ", + " =%@@#. *@@@# .#@@%= ", + " :=. :*@@= *@@@# =@@+: .=: ", + " %@#=..*# +@@@# #*..=#@# ", + " .@@@@+=# .%@%: #=+@@@@. ", + " .....=# -@= *+...:. ", + " -*%*-@= - =@-*%*- ", + " -@*. -@%. :%@- :*@- ", + " .#@#@* ", + " -#- ", + " ", +} + +local lines = math.max(#logo, #info) +for i = 1, lines do + local logo_str = logo[i] or string.rep(" ", 36) + + -- print logo segment in cyan + c(C_LOGO) + printInline(logo_str) + + -- print separator pipe + c(C_LABEL) + printInline("| ") + + -- print info segment + local row = info[i] + if row then + if row[1] == nil and i == 1 then + -- user@host line + c(C_USER) + printInline(row[2]) + elseif row[1] == nil and i == 2 then + -- separator line + c(C_SEP) + printInline(row[2]) + elseif row[1] then + -- label: value + c(C_LABEL) + printInline(row[1] .. ": ") + c(C_WHITE) + printInline(row[2]) + end + end + + c(C_WHITE) + print("") +end diff --git a/Src/Hyperion-bash/bin/hysh b/Src/Hyperion-bash/bin/hysh new file mode 100644 index 0000000..f9c6fdf --- /dev/null +++ b/Src/Hyperion-bash/bin/hysh @@ -0,0 +1,299 @@ +--:Minify:-- +syscall.open("/dev/tty/TTY1","r") --stdin (Device 0) +syscall.open("/dev/tty/TTY1","w") --stdout (Device 1) +syscall.open("/dev/null","w") --stderr (device 2) + +local success, errorMsg = xpcall(function() + + + +local fs = require("sys.fs") + +syscall.devctl(1,"clear") +syscall.devctl(1,"sfgc",1) +syscall.devctl(1,"spos",1,1) +print("HyperionOS hysh Shell") + +local userhost = (syscall.getUsername() or "Unknown").."@"..(syscall.getHostname() or "Unknown") +local commandHistory = {} +local terminate = false +syscall.setEnviron("SHELL","rtbash") +syscall.setEnviron("PATH","/bin/") +syscall.chdir("/") +local oldWD = "" + +for i = 1, 16 do + syscall.devctl(1,"sbgc",i) + printInline(" "); +end +print("\n") + +syscall.sigcatch(function(sig) + if sig == 1 then + terminate = true + end +end) + +local builtinCmds = {} + +builtinCmds.cd = function(path) + local cwd = syscall.getcwd() + local dirIn = (path or "") + if dirIn == "-" then + if oldWD == "" then + print("hysh-cd: No previous working directory set.") + else + print(oldWD) + syscall.chdir(oldWD) + oldWD = cwd + end + return + end + local dirInMod = dirIn + if dirIn:sub(1, 1) ~= "/" then dirInMod = cwd .. "/" .. dirIn end + local parts = {} + for part in dirInMod:gmatch("[^/]+") do + if part == ".." then + if #parts > 0 then table.remove(parts) end + elseif part ~= "." and part ~= "" then + table.insert(parts, part) + end + end + local normDir = "/" .. table.concat(parts, "/") + if normDir:sub(#normDir, #normDir) ~= "/" then normDir = normDir .. "/" end + + if not fs.isDir(normDir) then + print("hysh-cd: "..dirIn..": No such directory.") + return + end + oldWD = cwd + syscall.chdir(normDir) +end + + +local function getUserInput() + syscall.devctl(1,"sfgc",3) + syscall.write(1, userhost) + syscall.devctl(1,"sfgc",1) + syscall.write(1, ":") + syscall.devctl(1,"sfgc",10) + syscall.write(1, syscall.getcwd()) + syscall.devctl(1,"sfgc",1) + syscall.write(1, "$ ") + local curOffsetStr = syscall.devctl(1, "gpos") + local curOffsetX = tonumber(curOffsetStr:sub(1, curOffsetStr:find(";")-1)) + local curOffsetY = tonumber(curOffsetStr:sub(curOffsetStr:find(";")+1)) + + local input = "" + local blinkState = false + local cursorPos = 1 + local history = 0 + local dirty = true -- redraw on first iteration + + local function redraw() + syscall.devctl(1,"spos",curOffsetX,curOffsetY) + -- text before cursor + syscall.write(1, string.sub(input, 1, cursorPos-1)) + -- cursor character (inverted if blinking on) + if blinkState then + syscall.devctl(1,"sfgc",16) + syscall.devctl(1,"sbgc",1) + end + if cursorPos > #input then + syscall.write(1, " ") + else + syscall.write(1, string.sub(input, cursorPos, cursorPos)) + end + syscall.devctl(1,"sfgc",1) + syscall.devctl(1,"sbgc",16) + -- text after cursor + trailing space to erase old chars + syscall.write(1, string.sub(input, cursorPos+1) .. " ") + end + + while true do + local key = syscall.read(0) + if key and key ~= "" then + if key == "\19" then -- left arrow + if cursorPos > 1 then + cursorPos = cursorPos - 1 + dirty = true + end + elseif key == "\20" then -- right arrow + if cursorPos <= #input then + cursorPos = cursorPos + 1 + dirty = true + end + elseif key == "\17" then -- up arrow + if history < #commandHistory then + history = history + 1 + input = commandHistory[#commandHistory - history + 1] + cursorPos = #input + 1 + dirty = true + end + elseif key == "\18" then -- down arrow + if history > 1 then + history = history - 1 + input = commandHistory[#commandHistory - history + 1] + cursorPos = #input + 1 + dirty = true + elseif history == 1 then + history = 0 + input = "" + cursorPos = 1 + dirty = true + end + elseif key == "\b" then + if cursorPos > 1 then + input = string.sub(input, 1, cursorPos-2) .. string.sub(input, cursorPos) + cursorPos = cursorPos - 1 + dirty = true + end + elseif key == "\n" then + -- redraw cleanly with no cursor highlight before committing + syscall.devctl(1,"sfgc",1) + syscall.devctl(1,"sbgc",16) + syscall.devctl(1,"spos",curOffsetX,curOffsetY) + syscall.write(1, input .. " \n") + return input + else + input = string.sub(input, 1, cursorPos-1) .. key .. string.sub(input, cursorPos) + cursorPos = cursorPos + 1 + dirty = true + end + end + + -- cursor blink + local curBlink = ((math.floor(syscall.getUptime() / 500) % 2) == 0) + if curBlink ~= blinkState then + blinkState = curBlink + dirty = true + end + + if dirty then + redraw() + dirty = false + end + end +end + +local function runCommand(command) + do + local func = load("return " .. command, "@equation", "t", {}) + if func then + local success, result = pcall(func) + if success and type(result) == "number" then + print(result) + return + end + end + end + + terminate = false + local args = string.split(command, " ") + if builtinCmds[args[1]] then + local success, msg = pcall(builtinCmds[args[1]], table.unpack(args, 2)) + if not success then + local errSL = string.sub(msg, string.find(msg, "]") + 2) + syscall.devctl(1,"sfgc",2) + printInline(args[1]..": Program runtime error on line ") + print(string.sub(errSL, 1, string.find(errSL, ":") - 1)) + syscall.devctl(1,"sfgc",1) + print(string.sub(errSL, string.find(errSL, ":") + 1)) + end + return + end + + local cmdPath = "" + if string.find(args[1], "/") then + if fs.exists(args[1]) then + cmdPath = args[1] + end + else + local paths = string.split(syscall.getEnviron("PATH"), ":") + for _, path in pairs(paths) do + if fs.exists(path..args[1]) then + cmdPath = path..args[1] + break + end + end + end + + if cmdPath == "" then + print(args[1]..": Command not found") + return + end + + local progName = string.sub(cmdPath, #cmdPath - string.find(string.reverse(cmdPath), "/") + 2) + + local text = fs.readAllText(cmdPath) + local program, err = load(text, progName) + if not program then + local errSL = string.sub(err, string.find(err, ":") + 1) + syscall.devctl(1,"sfgc",2) + printInline(progName..": Program load error on line ") + print(string.sub(errSL, 1, string.find(errSL, ":") - 1)) + syscall.devctl(1,"sfgc",1) + print(string.sub(errSL, string.find(errSL, ":") + 1)) + return + end + + local proc = syscall.spawn(function(...) + syscall.open("/dev/tty/TTY1","r") + syscall.open("/dev/tty/TTY1","w") + syscall.open("/dev/null","w") + local success, msg = pcall(program, ...) + if not success then + local errSL = string.sub(msg, string.find(msg, ":") + 1) + syscall.devctl(1,"sfgc",2) + printInline(progName..": Program runtime error on line ") + print(string.sub(errSL, 1, string.find(errSL, ":") - 1)) + syscall.devctl(1,"sfgc",1) + print(string.sub(errSL, string.find(errSL, ":") + 1)) + end + end, progName, nil, {table.unpack(args, 2)}) + + while true do + local exited, code = syscall.collect(proc) + if exited then + if code then + print("\nTask exited with code:\n"..tostring(code)) + end + return + end + if terminate then + local success, err = syscall.kill(proc) + if success then + syscall.devctl(1,"sbgc",16) + syscall.devctl(1,"sfgc",2) + print("\nProgram Terminated.") + syscall.devctl(1,"sfgc",1) + end + terminate = false + break + end + sleep(0.05) + end +end + +while true do + local command = getUserInput() + if command ~= "" then + if command ~= commandHistory[#commandHistory] then + table.insert(commandHistory, command) + end + runCommand(command) + end +end + + +--ERROR HANDLING +end, debug.traceback) + +if not success then + syscall.log("Error running shell: "..errorMsg, "ERROR") + syscall.devctl(1,"sfgc",2) + syscall.devctl(1,"sbgc",16) + print() + print("Error running shell: ") + print(errorMsg) +end diff --git a/Src/Hyperion-bash/bin/hyshex b/Src/Hyperion-bash/bin/hyshex new file mode 100644 index 0000000..0ff03ce --- /dev/null +++ b/Src/Hyperion-bash/bin/hyshex @@ -0,0 +1,94 @@ +--:Minify:-- +syscall.open("/dev/tty/TTY1","r") +syscall.open("/dev/tty/TTY1","w") +syscall.open("/dev/null","r") +syscall.devctl(1,"clear") +syscall.devctl(1,"sfgc",1) +syscall.devctl(1,"spos",1,1) +print("HyperionOS hysh Shell") +local str="" +local stopInput=false +local proc=0 +local fs=require("sys.fs") +local timeout=false +syscall.setEnviron("SHELL","simpleshell") +printInline("> ") +syscall.sigcatch(function(sig) + if sig==1 then + syscall.kill(proc) + print("Terminated") + printInline("> ") + stopInput=false + end +end) + +while true do + if not stopInput then + local input=syscall.read(0) + if input then + if input=="\b" then + if #str>0 then + str=str:sub(1,#str-1) + printInline("\b") + end + elseif input=="\n" then + print("") + stopInput=true + if str == "" then + printInline("> ") + stopInput=false + else + local path=nil + local split=string.split(str, " ") + if fs.exists("/bin/"..split[1]) then + path="/bin/"..split[1] + elseif fs.exists("/bin/"..split[1]..".lua") then + path="/bin/"..split[1]..".lua" + end + if not path then + print("Program not found") + printInline("> ") + stopInput=false + else + local text = fs.readAllText(path) + local program, err = load(text, path) + if not program then + print(err) + printInline("> ") + end + proc = syscall.spawn(function(...) + syscall.open("/dev/tty/TTY1","r") + syscall.open("/dev/tty/TTY1","w") + syscall.open("/dev/null","w") + program(...) + end, path, nil, {table.unpack(split, 2)}) + end + str="" + end + else + str=str..input + printInline(input) + end + timeout=false + else + timeout=true + end + else + local exited, code = syscall.collect(proc) + if exited then + if code then + print("\nTask exited with code:\n"..tostring(code)) + end + printInline("> ") + stopInput=false + end + timeout=true + end + if timeout then + if stopInput then + sleep(.5) + else + sleep(.05) + end + end +end diff --git a/Src/Hyperion-bash/bin/id b/Src/Hyperion-bash/bin/id new file mode 100644 index 0000000..d183dda --- /dev/null +++ b/Src/Hyperion-bash/bin/id @@ -0,0 +1,19 @@ +--:Minify:-- +local args = {...} +local uid + +if args[1] then + uid = syscall.auth_getuid(args[1]) + if not uid then + print("id: user '" .. args[1] .. "' does not exist") + syscall.exit(1); return + end +else + uid = syscall.getuid() +end + +local pwent = syscall.auth_getpasswd(uid) +local name = (pwent and pwent.username) or tostring(uid) +local gid = (pwent and pwent.gid) or uid + +print(string.format("uid=%d(%s) gid=%d(%s)", uid, name, gid, name)) diff --git a/Src/Hyperion-bash/bin/login b/Src/Hyperion-bash/bin/login new file mode 100644 index 0000000..2f4d4a9 --- /dev/null +++ b/Src/Hyperion-bash/bin/login @@ -0,0 +1,178 @@ +--:Minify:-- +syscall.open("/dev/tty/TTY1","r") --stdin (fd 0) +syscall.open("/dev/tty/TTY1","w") --stdout (fd 1) +syscall.open("/dev/null","w") --stderr (fd 2) + +local fs = require("sys.fs") + +local MAX_ATTEMPTS = 3 + +local function readLine(mask) + local input = "" + while true do + local ch = syscall.read(0) + if not ch or ch == "" then + -- buffer empty, spin + elseif ch == "\n" then + syscall.write(1, "\n") + return input + elseif ch == "\b" then + if #input > 0 then + input = input:sub(1, -2) + syscall.write(1, "\b \b") + end + else + input = input .. ch + syscall.write(1, mask or ch) + end + end +end + +local function firstBoot() + local shadow = fs.readAllText("/etc/shadow") or "" + if shadow:match("%S") then return end + + syscall.devctl(1, "clear") + syscall.devctl(1, "spos", 1, 1) + syscall.devctl(1, "sfgc", 3) + syscall.write(1, "HyperionOS First Boot Setup\n") + syscall.devctl(1, "sfgc", 1) + syscall.write(1, "No root password is set. Please create one now.\n\n") + + while true do + syscall.write(1, "New root password: ") + local pw1 = readLine("*") + syscall.write(1, "Confirm password: ") + local pw2 = readLine("*") + + if pw1 ~= pw2 then + syscall.devctl(1, "sfgc", 2) + syscall.write(1, "Passwords do not match. Try again.\n\n") + syscall.devctl(1, "sfgc", 1) + elseif #pw1 < 6 then + syscall.devctl(1, "sfgc", 2) + syscall.write(1, "Password too short (minimum 6 characters).\n\n") + syscall.devctl(1, "sfgc", 1) + else + local ok, err = syscall.auth_setpassword(0, pw1) + if ok then + syscall.devctl(1, "sfgc", 3) + syscall.write(1, "Root password set.\n\n") + syscall.devctl(1, "sfgc", 1) + sleep(0.5) + break + else + syscall.devctl(1, "sfgc", 2) + syscall.write(1, "Error: " .. tostring(err) .. "\n") + syscall.devctl(1, "sfgc", 1) + end + end + end +end + +local function spawnShell(username, uid, shell, homedir) + local shellText = fs.readAllText(shell) + if not shellText then + syscall.write(1, "login: shell not found: " .. shell .. "\n") + sleep(2) + return false + end + + -- Spawn a wrapper that loads and runs the shell, reporting any error back + -- via exit code channel so we can display it + local errFifo = {} + + local proc = syscall.spawn(function() + syscall.setuid(uid) + syscall.chdir(homedir) + syscall.setEnviron("HOME", homedir) + syscall.setEnviron("USER", username) + syscall.setEnviron("SHELL", shell) + syscall.setEnviron("PATH", "/bin/") + + local shellFn, loadErr = load(shellText, "@" .. shell) + if not shellFn then + -- Report load error via log and a recognizable exit code + syscall.log("login: shell load error: " .. tostring(loadErr), "ERROR") + syscall.exit(-1) + return + end + + local ok, runErr = xpcall(shellFn, debug.traceback) + if not ok then + syscall.log("login: shell runtime error: " .. tostring(runErr), "ERROR") + end + end, username .. ":shell") + + while true do + local exited, code = syscall.collect(proc) + if exited then + if code then + syscall.devctl(1, "sfgc", 2) + syscall.write(1, "\nShell exited with code: " .. tostring(code) .. "\n") + syscall.write(1, "(Check /var/log/syslog.log for details)\n") + syscall.devctl(1, "sfgc", 1) + sleep(2) + end + return true + end + sleep(0.1) + end +end + +local function doLogin() + syscall.devctl(1, "clear") + syscall.devctl(1, "sfgc", 1) + syscall.devctl(1, "sbgc", 16) + syscall.devctl(1, "spos", 1, 1) + + local hostname = syscall.getHostname() or "hyperion" + syscall.write(1, "HyperionOS\n") + syscall.write(1, hostname .. " login\n\n") + + local attempts = 0 + while attempts < MAX_ATTEMPTS do + syscall.devctl(1, "sfgc", 1) + syscall.write(1, "Username: ") + local username = readLine(nil) + + if username == "" then goto continue end + + syscall.write(1, "Password: ") + local password = readLine("*") + + local ok, err = syscall.auth_login(username, password) + if ok then + local uid = syscall.auth_getuid(username) + local pwent = uid and syscall.auth_getpasswd(uid) + local shell = (pwent and pwent.shell) or "/bin/hysh" + local homedir = (pwent and pwent.homedir) or "/" + + syscall.devctl(1, "sfgc", 3) + syscall.write(1, "\nWelcome, " .. username .. "!\n") + syscall.devctl(1, "sfgc", 1) + sleep(0.3) + + spawnShell(username, uid, shell, homedir) + return -- back to login prompt + else + attempts = attempts + 1 + sleep(1) + syscall.devctl(1, "sfgc", 2) + syscall.write(1, "Login incorrect.\n\n") + syscall.devctl(1, "sfgc", 1) + end + + ::continue:: + end + + syscall.devctl(1, "sfgc", 2) + syscall.write(1, "Maximum login attempts exceeded.\n") + syscall.devctl(1, "sfgc", 1) + sleep(5) +end + +firstBoot() +while true do + doLogin() +end diff --git a/Src/Hyperion-bash/bin/lsusers b/Src/Hyperion-bash/bin/lsusers new file mode 100644 index 0000000..1685222 --- /dev/null +++ b/Src/Hyperion-bash/bin/lsusers @@ -0,0 +1,19 @@ +--:Minify:-- +local users = syscall.auth_listusers() +if not users or #users == 0 then + print("No users found.") + return +end + +syscall.devctl(1,"sfgc",13) +print(string.format("%-6s %-6s %-16s %-20s %s", "UID", "GID", "Username", "Home", "Shell")) +print(string.rep("-", 65)) +syscall.devctl(1,"sfgc",1) + +for _, u in ipairs(users) do + local lock_marker = u.locked and " [locked]" or "" + if u.locked then syscall.devctl(1,"sfgc",2) end + print(string.format("%-6d %-6d %-16s %-20s %s%s", + u.uid, u.gid, u.username, u.homedir, u.shell, lock_marker)) + if u.locked then syscall.devctl(1,"sfgc",1) end +end diff --git a/Src/Hyperion-bash/bin/lua b/Src/Hyperion-bash/bin/lua index f086d89..e10d3ff 100644 --- a/Src/Hyperion-bash/bin/lua +++ b/Src/Hyperion-bash/bin/lua @@ -1,149 +1,325 @@ -syscall.devctl(1,"sfgc",7) -print("HyperionOS Lua prompt.") -print("Call exit() to exit.") +--:Minify:-- +local C_PROMPT = 7 +local C_CONT = 13 +local C_OUT = 5 +local C_ERR = 2 +local C_KEY = 3 +local C_STR = 9 +local C_NUM = 10 +local C_BOOL = 8 +local C_NIL = 12 +local C_TABLE = 13 -local commandHistory = {} -local luaEnv=setmetatable({ - ["exit"] = setmetatable({}, { - __tostring = function() return "Call exit() to exit." end, - __call = function() syscall.exit() end, - }), - ["_echo"] = function(...) - return ... - end, -},{__index=_ENV}) +local function c(col) syscall.devctl(1, "sfgc", col) end +local function w(s) syscall.write(1, tostring(s)) end -local function getUserInput() - syscall.devctl(1,"sfgc",1) - printInline("lua> ") - local curOffsetStr = syscall.devctl(1, "gpos") - local curOffsetX = tonumber(curOffsetStr:sub(1, curOffsetStr:find(";")-1)) - local curOffsetY = tonumber(curOffsetStr:sub(curOffsetStr:find(";")+1)) +local MAX_DEPTH = 6 +local MAX_ENTRIES = 64 - local input = "" - local blinkState = false - local cursorPos = 1 - local history = 0 +local function prettyVal(val, indent, seen) + indent = indent or 0 + seen = seen or {} + local t = type(val) - while true do - local key=syscall.read(0) - if key then - if key == "\19" then --TODO: REPLACE WITH LEFT ARROW - if cursorPos > 1 then - cursorPos = cursorPos - 1 - end - elseif key == "\20" then --TODO: REPLACE WITH RIGHT ARROW - if cursorPos <= #input then - cursorPos = cursorPos + 1 - end - elseif key == "\17" then --TODO: REPLACE WITH UP ARROW - if history < #commandHistory then - syscall.devctl(1,"spos",curOffsetX,curOffsetY) - printInline((" "):rep(#input + 1)) - history = history + 1 - input = commandHistory[#commandHistory - history + 1] - cursorPos = #input + 1 - end - elseif key == "\18" then --TODO: REPLACE WITH DOWN ARROW - if history > 1 then - syscall.devctl(1,"spos",curOffsetX,curOffsetY) - printInline((" "):rep(#input + 1)) - history = history - 1 - input = commandHistory[#commandHistory - history + 1] - cursorPos = #input + 1 - elseif history == 1 then - syscall.devctl(1,"spos",curOffsetX,curOffsetY) - printInline((" "):rep(#input + 1)) - history = 0 - input = "" - cursorPos = 1 - end - elseif key == "\b" then - if cursorPos > 1 then - syscall.devctl(1,"spos",curOffsetX,curOffsetY) - printInline((" "):rep(#input + 1)) - input = string.sub(input, 1, cursorPos-2)..string.sub(input, cursorPos) - cursorPos = cursorPos - 1 - end - elseif key == "\n" then - syscall.devctl(1,"spos",curOffsetX,curOffsetY) - print(input.." ") - return input - else - input = string.sub(input, 1, cursorPos-1)..key..string.sub(input, cursorPos) - cursorPos = cursorPos + 1 - end - local screenSizeStr = syscall.devctl(1, "size") - local sizeX = tonumber(screenSizeStr:sub(1, screenSizeStr:find(";")-1)) - local sizeY = tonumber(screenSizeStr:sub(screenSizeStr:find(";")+1)) - local totalChars = sizeX * sizeY - local eocCharNum = ((curOffsetY - 1) * sizeX) + curOffsetX + #input - if eocCharNum >= totalChars then - syscall.devctl(1,"spos",sizeX,sizeY) - printInline(" ") - curOffsetY = curOffsetY - 1 - end - end - syscall.devctl(1,"spos",curOffsetX,curOffsetY) - printInline(string.sub(input, 1, cursorPos-1)) - if blinkState then - syscall.devctl(1,"sfgc",16) - syscall.devctl(1,"sbgc",1) - end - if cursorPos > #input then - printInline(" ") + if t == "nil" then + c(C_NIL); w("nil") + elseif t == "boolean" then + c(C_BOOL); w(tostring(val)) + elseif t == "number" then + c(C_NUM) + if val ~= val then + w("nan") + elseif val == math.huge then + w("inf") + elseif val == -math.huge then + w("-inf") + elseif val == math.floor(val) and math.abs(val) < 1e15 then + w(tostring(math.floor(val))) else - printInline(string.sub(input, cursorPos, cursorPos)) + w(tostring(val)) end - syscall.devctl(1,"sfgc",1) - syscall.devctl(1,"sbgc",16) - printInline(string.sub(input, cursorPos+1)) - if cursorPos <= #input then - printInline(" ") + elseif t == "string" then + c(C_STR) + local s = string.format("%q", val) + w(s) + elseif t == "function" then + c(C_TABLE); w(tostring(val)) + elseif t == "table" then + if seen[val] then + c(C_TABLE); w("") + return end - local curBlink = ((math.floor(syscall.getUptime() / 500) % 2) == 0) - if curBlink ~= blinkState then - blinkState = curBlink + if indent >= MAX_DEPTH then + c(C_TABLE); w("") + return end + seen[val] = true + + local pad = string.rep(" ", indent) + local padIn = string.rep(" ", indent + 1) + + local arrKeys = {} + local hashKeys = {} + local arrMax = #val + + for k in pairs(val) do + if type(k) == "number" and k >= 1 and k <= arrMax and k == math.floor(k) then + arrKeys[#arrKeys+1] = k + else + hashKeys[#hashKeys+1] = k + end + end + table.sort(arrKeys) + + table.sort(hashKeys, function(a, b) + local ta, tb = type(a), type(b) + if ta == tb then + if ta == "string" then return a < b end + if ta == "number" then return a < b end + return tostring(a) < tostring(b) + end + return ta < tb + end) + + local total = #arrKeys + #hashKeys + if total == 0 then + c(C_TABLE); w("{}") + seen[val] = nil + return + end + + c(C_TABLE); w("{\n") + + local shown = 0 + local function printEntry(k, v, isLast) + shown = shown + 1 + if shown > MAX_ENTRIES then return true end + w(padIn) + if type(k) == "number" and arrKeys[k] then + else + if type(k) == "string" and k:match("^[a-zA-Z_][a-zA-Z0-9_]*$") then + c(C_KEY); w(k) + else + c(C_TABLE); w("[") + prettyVal(k, indent+1, seen) + c(C_TABLE); w("]") + end + c(C_TABLE); w(" = ") + end + prettyVal(v, indent+1, seen) + c(C_TABLE) + if not isLast then w(",") end + w("\n") + end + + for i, k in ipairs(arrKeys) do + w(padIn) + prettyVal(val[k], indent+1, seen) + c(C_TABLE) + if i < total then w(",") end + w("\n") + shown = shown + 1 + if shown >= MAX_ENTRIES then + c(C_NIL); w(padIn .. "-- ..." .. (total - shown) .. " more entries\n") + break + end + end + + if shown < MAX_ENTRIES then + for i, k in ipairs(hashKeys) do + local isLast = (shown + 1 >= total) + w(padIn) + if type(k) == "string" and k:match("^[a-zA-Z_][a-zA-Z0-9_]*$") then + c(C_KEY); w(k) + else + c(C_TABLE); w("[") + prettyVal(k, indent+1, seen) + c(C_TABLE); w("]") + end + c(C_TABLE); w(" = ") + prettyVal(val[k], indent+1, seen) + c(C_TABLE) + shown = shown + 1 + if shown < total then w(",") end + w("\n") + if shown >= MAX_ENTRIES then + local rem = total - shown + if rem > 0 then + c(C_NIL); w(padIn .. "-- ..." .. rem .. " more entries\n") + end + break + end + end + end + + c(C_TABLE); w(pad .. "}") + seen[val] = nil + else + c(C_TABLE); w(tostring(val)) end + c(1) +end + +local function printResults(...) + local n = select("#", ...) + if n == 0 then return end + for i = 1, n do + if i > 1 then c(C_TABLE); w("\t") end + prettyVal(select(i, ...), 0, {}) + end + w("\n") + c(1) +end + +local luaEnv = setmetatable({}, {__index = _ENV}) +luaEnv._G = luaEnv + +luaEnv.print = function(...) + local n = select("#", ...) + for i = 1, n do + if i > 1 then w("\t") end + prettyVal(select(i, ...), 0, {}) + end + w("\n") + c(1) +end + +luaEnv.pp = function(val) + prettyVal(val, 0, {}) + w("\n") + c(1) +end + +luaEnv.exit = setmetatable({}, { + __tostring = function() return "function: exit()" end, + __call = function() syscall.exit() end, +}) + +local function compile(code) + local exprFn = load("return " .. code, "@lua", "t", luaEnv) + if exprFn then return exprFn, true end + local stmtFn, err = load(code, "@lua", "t", luaEnv) + return stmtFn, false, err +end + +local function isIncomplete(code) + local _, err = load(code, "@lua", "t", luaEnv) + return err and (err:find("") ~= nil or err:find("'end'") ~= nil + or err:find("'then'") ~= nil or err:find("'until'") ~= nil) +end + +local function cleanErr(msg) + return tostring(msg) + :gsub("^%[string .-%]:", "") + :gsub("^@lua:", "") + :gsub("stack traceback:.*", "") + :match("^%s*(.-)%s*$") end local function runCode(code) - local func, err = load(code, "@lua", "t", luaEnv) - local isReturn = false - if load("return "..code) then - func, err = load("return _echo("..code.."\n)", "@lua", "t", luaEnv) - isReturn = true - end - if not func then - local errSL = string.sub(err, string.find(err, ":") + 1) - syscall.devctl(1,"sfgc",2) - printInline("@lua: Load error on line ") - print(string.sub(errSL, 1, string.find(errSL, ":") - 1)) - syscall.devctl(1,"sfgc",1) - print(string.sub(errSL, string.find(errSL, ":") + 1)) + local fn, isExpr, err = compile(code) + if not fn then + c(C_ERR); w("[error] "); c(1); w(cleanErr(err) .. "\n") return end - local success, msg = xpcall(func, debug.traceback) - if not success then - local errSL = string.sub(msg, string.find(msg, ":") + 1) - syscall.devctl(1,"sfgc",2) - printInline("@lua: Runtime error on line ") - print(string.sub(errSL, 1, string.find(errSL, ":") - 1)) - syscall.devctl(1,"sfgc",1) - print(string.sub(errSL, string.find(errSL, ":") + 1)) - elseif isReturn then - print(tostring(msg)) + local results = table.pack(xpcall(fn, debug.traceback)) + local ok = table.remove(results, 1) + results.n = results.n - 1 + + if not ok then + c(C_ERR); w("[error] "); c(1); w(cleanErr(results[1]) .. "\n") + elseif isExpr and results.n > 0 then + c(C_OUT); w("= ") + printResults(table.unpack(results, 1, results.n)) end end -while true do - local code = getUserInput() - if code ~= "" then - if code ~= commandHistory[#commandHistory] then - table.insert(commandHistory, code) - end - runCode(code) +local function getUserInput(prompt, history) + c(C_PROMPT); w(prompt); c(1) + local pos = syscall.devctl(1, "gpos") + local ox = tonumber(pos:sub(1, pos:find(";")-1)) + local oy = tonumber(pos:sub(pos:find(";")+1)) + + local input = "" + local cursor = 1 + local histIdx = 0 + local blink = false + local dirty = true + + local function redraw() + syscall.devctl(1, "spos", ox, oy) + w(input:sub(1, cursor-1)) + if blink then syscall.devctl(1,"sfgc",16); syscall.devctl(1,"sbgc",1) end + w(cursor > #input and " " or input:sub(cursor, cursor)) + syscall.devctl(1,"sfgc",1); syscall.devctl(1,"sbgc",16) + w(input:sub(cursor+1) .. " ") + dirty = false end -end \ No newline at end of file + + while true do + local key = syscall.read(0) + if key and key ~= "" then + if key == "\19" then + if cursor > 1 then cursor = cursor - 1; dirty = true end + elseif key == "\20" then + if cursor <= #input then cursor = cursor + 1; dirty = true end + elseif key == "\17" then + if history and histIdx < #history then + histIdx = histIdx + 1 + input = history[#history - histIdx + 1] + cursor = #input + 1; dirty = true + end + elseif key == "\18" then + if histIdx > 1 then + histIdx = histIdx - 1 + input = history[#history - histIdx + 1] + cursor = #input + 1; dirty = true + elseif histIdx == 1 then + histIdx = 0; input = ""; cursor = 1; dirty = true + end + elseif key == "\b" then + if cursor > 1 then + input = input:sub(1, cursor-2) .. input:sub(cursor) + cursor = cursor - 1; dirty = true + end + elseif key == "\n" then + syscall.devctl(1,"sfgc",1); syscall.devctl(1,"sbgc",16) + syscall.devctl(1,"spos",ox,oy) + w(input .. " \n") + return input + else + input = input:sub(1, cursor-1) .. key .. input:sub(cursor) + cursor = cursor + 1; dirty = true + end + end + local nb = (math.floor(syscall.getUptime() / 500) % 2) == 0 + if nb ~= blink then blink = nb; dirty = true end + if dirty then redraw() end + end +end + +syscall.devctl(1, "clear") +syscall.devctl(1, "spos", 1, 1) +c(C_PROMPT); w("HyperionOS " .. _VERSION .. "\n") +c(C_NIL) +w("Interactive Lua REPL. exit() to quit.\n\n") +c(1) + +local history = {} + +while true do + local code = getUserInput("lua> ", history) + if code == "" then goto continue end + + while isIncomplete(code) do + code = code .. "\n" .. getUserInput("... ", nil) + end + + if code ~= history[#history] then + history[#history+1] = code + end + + runCode(code) + ::continue:: +end diff --git a/Src/Hyperion-bash/bin/passwd b/Src/Hyperion-bash/bin/passwd new file mode 100644 index 0000000..4e03d9f --- /dev/null +++ b/Src/Hyperion-bash/bin/passwd @@ -0,0 +1,80 @@ +--:Minify:-- +-- passwd: change a user's password +-- Usage: passwd [username] (default: current user) + +local args = {...} +local targetName = args[1] + +local currentUid = syscall.getuid() + +local targetUid +if targetName then + targetUid = syscall.auth_getuid(targetName) + if not targetUid then + print("passwd: user '" .. targetName .. "' does not exist") + syscall.exit(1); return + end + -- Only root can change another user's password + if currentUid ~= 0 and targetUid ~= currentUid then + print("passwd: permission denied") + syscall.exit(1); return + end +else + targetUid = currentUid + targetName = syscall.getUsername(currentUid) or tostring(currentUid) +end + +-- Non-root must verify their current password first +if currentUid ~= 0 then + printInline("Current password: ") + local cur = "" + while true do + local ch = syscall.read(0) + if not ch or ch == "" then + elseif ch == "\n" then syscall.write(1,"\n"); break + elseif ch == "\b" then + if #cur > 0 then cur=cur:sub(1,-2); syscall.write(1,"\b \b") end + else cur=cur..ch; syscall.write(1,"*") end + end + local ok, err = syscall.auth_elevate(targetName, cur) + if not ok then + sleep(1) + print("passwd: authentication failure") + syscall.exit(1); return + end +end + +printInline("New password: ") +local pw1 = "" +while true do + local ch = syscall.read(0) + if not ch or ch == "" then + elseif ch == "\n" then syscall.write(1,"\n"); break + elseif ch == "\b" then + if #pw1 > 0 then pw1=pw1:sub(1,-2); syscall.write(1,"\b \b") end + else pw1=pw1..ch; syscall.write(1,"*") end +end + +printInline("Confirm password: ") +local pw2 = "" +while true do + local ch = syscall.read(0) + if not ch or ch == "" then + elseif ch == "\n" then syscall.write(1,"\n"); break + elseif ch == "\b" then + if #pw2 > 0 then pw2=pw2:sub(1,-2); syscall.write(1,"\b \b") end + else pw2=pw2..ch; syscall.write(1,"*") end +end + +if pw1 ~= pw2 then + print("passwd: passwords do not match") + syscall.exit(1); return +end + +local ok, err = syscall.auth_setpassword(targetUid, pw1) +if not ok then + print("passwd: " .. tostring(err)) + syscall.exit(1); return +end + +print("passwd: password updated for '" .. targetName .. "'") diff --git a/Src/Hyperion-bash/bin/su b/Src/Hyperion-bash/bin/su new file mode 100644 index 0000000..1517c62 --- /dev/null +++ b/Src/Hyperion-bash/bin/su @@ -0,0 +1,72 @@ +--:Minify:-- +local fs = require("sys.fs") + +local targetUser = ({...})[1] or "root" + +local currentUid = syscall.getuid() +local currentUser = syscall.getUsername(currentUid) or tostring(currentUid) + +local targetUid = syscall.auth_getuid(targetUser) +if not targetUid then + print("su: user '" .. targetUser .. "' does not exist") + syscall.exit(1) + return +end + +local ok, err +if currentUid == 0 then + ok = true +else + printInline("Password: ") + local pw = "" + while true do + local ch = syscall.read(0) + if not ch or ch == "" then + elseif ch == "\n" then + syscall.write(1, "\n") + break + elseif ch == "\b" then + if #pw > 0 then pw = pw:sub(1, -2); syscall.write(1, "\b \b") end + else + pw = pw .. ch + syscall.write(1, "*") + end + end + + ok, err = syscall.auth_elevate(targetUser, pw) + if not ok then + sleep(1) + print("su: Authentication failure") + syscall.exit(1) + return + end +end + +if currentUid == 0 then + syscall.setuid(targetUid) +end + +local pwent = syscall.auth_getpasswd(targetUid) +local shell = (pwent and pwent.shell) or "/bin/hysh" +local homedir = (pwent and pwent.homedir) or "/" + +syscall.chdir(homedir) +syscall.setEnviron("HOME", homedir) +syscall.setEnviron("USER", targetUser) +syscall.setEnviron("SHELL", shell) + +local shellText = fs.readAllText(shell) +if not shellText then + print("su: shell not found: " .. shell) + syscall.exit(1) + return +end + +local shellFn, loadErr = load(shellText, "@" .. shell) +if not shellFn then + print("su: cannot load shell: " .. tostring(loadErr)) + syscall.exit(1) + return +end + +shellFn() diff --git a/Src/Hyperion-bash/bin/sudo b/Src/Hyperion-bash/bin/sudo new file mode 100644 index 0000000..c57b6c3 --- /dev/null +++ b/Src/Hyperion-bash/bin/sudo @@ -0,0 +1,110 @@ +--:Minify:-- +local fs = require("sys.fs") + +local cmdArgs = {...} +local targetUser = "root" +local i = 1 + +if cmdArgs[i] == "-u" then + i = i + 1 + local uarg = cmdArgs[i] or "root" + local numUid = tonumber(uarg) + if numUid then + local pwent = syscall.auth_getpasswd(numUid) + targetUser = (pwent and pwent.username) or uarg + else + targetUser = uarg + end + i = i + 1 +end + +local cmd = cmdArgs[i] +if not cmd or cmd == "" then + print("usage: sudo [-u user] [args...]") + syscall.exit(1) + return +end + +local restArgs = {} +for j = i + 1, #cmdArgs do restArgs[#restArgs + 1] = cmdArgs[j] end + +local currentUid = syscall.getuid() +local currentUser = syscall.getUsername(currentUid) or tostring(currentUid) + +local targetUid = syscall.auth_getuid(targetUser) +if not targetUid then + print("sudo: user '" .. targetUser .. "' does not exist") + syscall.exit(1) + return +end + +if currentUid ~= 0 then + printInline("[sudo] password for " .. currentUser .. ": ") + local pw = "" + while true do + local ch = syscall.read(0) + if not ch or ch == "" then + elseif ch == "\n" then + syscall.write(1, "\n") + break + elseif ch == "\b" then + if #pw > 0 then pw = pw:sub(1, -2); syscall.write(1, "\b \b") end + else + pw = pw .. ch + syscall.write(1, "*") + end + end + + local ok, err = syscall.auth_elevate(currentUser, pw) + if not ok then + sleep(1) + print("sudo: Authentication failure") + syscall.exit(1) + return + end + + if targetUid ~= 0 then + syscall.setuid(targetUid) + end +else + if targetUid ~= currentUid then + syscall.setuid(targetUid) + end +end + +local cmdPath = "" +if cmd:find("/") then + if fs.exists(cmd) then cmdPath = cmd end +else + local paths = string.split(syscall.getEnviron("PATH") or "/bin/", ":") + for _, p in ipairs(paths) do + local full = p .. cmd + if fs.exists(full) then cmdPath = full; break end + end +end + +if cmdPath == "" then + print("sudo: command not found: " .. cmd) + syscall.exit(1) + return +end + +local text = fs.readAllText(cmdPath) +local program, loadErr = load(text, "@" .. cmdPath) +if not program then + print("sudo: cannot load " .. cmd .. ": " .. tostring(loadErr)) + syscall.exit(1) + return +end + +local pwent = syscall.auth_getpasswd(targetUid) +if pwent and pwent.homedir then + syscall.setEnviron("HOME", pwent.homedir) +end +syscall.setEnviron("USER", targetUser) + +local ok, err = xpcall(program, debug.traceback, table.unpack(restArgs)) +if not ok then + print("sudo: " .. cmd .. ": " .. tostring(err)) + syscall.exit(1) +end diff --git a/Src/Hyperion-bash/bin/useradd b/Src/Hyperion-bash/bin/useradd new file mode 100644 index 0000000..2baf4f2 --- /dev/null +++ b/Src/Hyperion-bash/bin/useradd @@ -0,0 +1,67 @@ +--:Minify:-- +local args = {...} +local i = 1 +local opt = { createHome = true } + +while i <= #args do + local a = args[i] + if a == "-p" then i=i+1; opt.password = args[i] + elseif a == "-g" then i=i+1; opt.gid = tonumber(args[i]) + elseif a == "-d" then i=i+1; opt.homedir = args[i] + elseif a == "-s" then i=i+1; opt.shell = args[i] + elseif a == "-M" then opt.createHome = false + elseif a:sub(1,1) ~= "-" then opt.username = a + else print("useradd: unknown option: " .. a); return end + i = i + 1 +end + +if not opt.username then + print("Usage: useradd [-p password] [-g gid] [-d homedir] [-s shell] [-M] ") + syscall.exit(1); return +end + +local password = opt.password +if not password then + printInline("New password: ") + password = "" + while true do + local ch = syscall.read(0) + if not ch or ch == "" then + elseif ch == "\n" then syscall.write(1,"\n"); break + elseif ch == "\b" then + if #password > 0 then password=password:sub(1,-2); syscall.write(1,"\b \b") end + else password=password..ch; syscall.write(1,"*") end + end + printInline("Confirm password: ") + local pw2 = "" + while true do + local ch = syscall.read(0) + if not ch or ch == "" then + elseif ch == "\n" then syscall.write(1,"\n"); break + elseif ch == "\b" then + if #pw2 > 0 then pw2=pw2:sub(1,-2); syscall.write(1,"\b \b") end + else pw2=pw2..ch; syscall.write(1,"*") end + end + if password ~= pw2 then + print("useradd: passwords do not match") + syscall.exit(1); return + end +end + +local uid, err = syscall.auth_newuser( + opt.username, password, opt.gid, opt.homedir, opt.shell or "/bin/hysh" +) +if not uid then + print("useradd: " .. tostring(err)) + syscall.exit(1); return +end + +if opt.createHome then + local home = opt.homedir or ("/home/" .. opt.username) + local ok, e = pcall(syscall.mkdir, home) + if not ok then + print("useradd: warning: could not create home " .. home .. ": " .. tostring(e)) + end +end + +print("useradd: created user '" .. opt.username .. "' with uid=" .. tostring(uid)) diff --git a/Src/Hyperion-bash/bin/userdel b/Src/Hyperion-bash/bin/userdel new file mode 100644 index 0000000..701c869 --- /dev/null +++ b/Src/Hyperion-bash/bin/userdel @@ -0,0 +1,49 @@ +--:Minify:-- +local args = {...} +local removeHome = false +local username = nil + +for _, a in ipairs(args) do + if a == "-r" then removeHome = true + elseif a:sub(1,1) ~= "-" then username = a + else print("userdel: unknown option: " .. a); syscall.exit(1); return end +end + +if not username then + print("Usage: userdel [-r] ") + syscall.exit(1); return +end + +local uid = syscall.auth_getuid(username) +if not uid then + print("userdel: user '" .. username .. "' does not exist") + syscall.exit(1); return +end + +local pwent = syscall.auth_getpasswd(uid) + +local ok, err = syscall.auth_deleteuser(uid) +if not ok then + print("userdel: " .. tostring(err)) + syscall.exit(1); return +end + +if removeHome and pwent and pwent.homedir then + local fs = require("sys.fs") + local ok2, err2 = pcall(function() + local function rmdir(path) + for _, f in ipairs(fs.list(path) or {}) do + local full = path .. "/" .. f + if fs.isDir(full) then rmdir(full) + else syscall.remove(full) end + end + syscall.remove(path) + end + if fs.exists(pwent.homedir) then rmdir(pwent.homedir) end + end) + if not ok2 then + print("userdel: warning: could not remove home: " .. tostring(err2)) + end +end + +print("userdel: deleted user '" .. username .. "'") diff --git a/Src/Hyperion-bash/bin/usermod b/Src/Hyperion-bash/bin/usermod new file mode 100644 index 0000000..f053cb7 --- /dev/null +++ b/Src/Hyperion-bash/bin/usermod @@ -0,0 +1,49 @@ +--:Minify:-- +local args = {...} +local i = 1 +local opt = {} + +while i <= #args do + local a = args[i] + if a == "-l" then i=i+1; opt.newname = args[i] + elseif a == "-p" then i=i+1; opt.password = args[i] + elseif a == "-g" then i=i+1; opt.gid = tonumber(args[i]) + elseif a == "-d" then i=i+1; opt.homedir = args[i] + elseif a == "-s" then i=i+1; opt.shell = args[i] + elseif a == "-L" then opt.lock = true + elseif a == "-U" then opt.unlock = true + elseif a:sub(1,1) ~= "-" then opt.username = a + else print("usermod: unknown option: " .. a); syscall.exit(1); return end + i = i + 1 +end + +if not opt.username then + print("Usage: usermod [-l newname] [-p password] [-g gid] [-d homedir] [-s shell] [-L] [-U] ") + syscall.exit(1); return +end + +if opt.lock and opt.unlock then + print("usermod: -L and -U are mutually exclusive") + syscall.exit(1); return +end + +local uid = syscall.auth_getuid(opt.username) +if not uid then + print("usermod: user '" .. opt.username .. "' does not exist") + syscall.exit(1); return +end + +local function apply(fn, ...) + local ok, err = fn(...) + if not ok then print("usermod: " .. tostring(err)); syscall.exit(1) end +end + +if opt.newname then apply(syscall.auth_setusername, uid, opt.newname) end +if opt.password then apply(syscall.auth_setpassword, uid, opt.password) end +if opt.gid then apply(syscall.auth_setgid, uid, opt.gid) end +if opt.homedir then apply(syscall.auth_sethomedir, uid, opt.homedir) end +if opt.shell then apply(syscall.auth_setshell, uid, opt.shell) end +if opt.lock then apply(syscall.auth_lockuser, uid) end +if opt.unlock then apply(syscall.auth_unlockuser, uid) end + +print("usermod: updated user '" .. opt.username .. "'") diff --git a/Src/Hyperion-firmware-cct/boot/cct/eeprom b/Src/Hyperion-firmware-cct/boot/cct/eeprom index 581280d..e3bb2a4 100644 --- a/Src/Hyperion-firmware-cct/boot/cct/eeprom +++ b/Src/Hyperion-firmware-cct/boot/cct/eeprom @@ -1,5 +1,4 @@ --:Minify:-- -sleep(1) local BOOT_DRIVE_PATH=({...})[1] or "/$" -- UnBIOS by JackMacWindows -- This will undo most of the changes/additions made in the BIOS, but some things may remain wrapped if `debug` is unavailable diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/01_stdlib.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/01_stdlib.kmod index c637b97..5ada732 100644 --- a/Src/Hyperion-kernel/lib/modules/Hyperion/01_stdlib.kmod +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/01_stdlib.kmod @@ -17,6 +17,7 @@ function string.delim(str, ...) return table.concat(table.pack(...), str) end function string.split(str, delim, maxResultCountOrNil) assert(#delim == 1, "only delim len 1 supported for now") + if not str then return false end maxResultCountOrNil = (maxResultCountOrNil or 0) - 1 local rv = {} local buf = "" diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/10_vfs.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/10_vfs.kmod index 81dbfd5..e641638 100644 --- a/Src/Hyperion-kernel/lib/modules/Hyperion/10_vfs.kmod +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/10_vfs.kmod @@ -334,7 +334,7 @@ function vfs.read(fd, count) local file = task.fd[fd] if not file then error("EBADF") end if not file.handle.read then error("EBADF") end - return file.handle.read(count or 1) + return file.handle.read(count or 1) or "" end -- Write @@ -354,7 +354,7 @@ function vfs.pread(fd, count, offset) if not file.handle.read then error("EBADF") end if not file.handle.seek then error("EBADF") end file.handle.seek("set", offset) - return file.handle.read(count or 1) + return file.handle.read(count or 1) or "" end function vfs.pwrite(fd, content, offset) diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/40_auth.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/40_auth.kmod index 4dbf725..71a9c43 100644 --- a/Src/Hyperion-kernel/lib/modules/Hyperion/40_auth.kmod +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/40_auth.kmod @@ -3,102 +3,66 @@ local kernel = ... local auth = {} kernel.auth = auth --- @SPSF work here - --- needed - --- login -- sets the current proccess to the specifyed user id --- setPassword -- sets the password for specifiyed user id --- setUsername -- sets --- newUser -- sets - --- PASSWD FILE FORMAT --- uid:gid:username:homedir:shell - --- SHADOW FILE FORMAT --- uid:salt:hash +-- PASSWD FILE FORMAT: uid:gid:username:homedir:shell +-- SHADOW FILE FORMAT: uid:salt:hash local function getFile(path) local file = kernel.vfs.open(path, "r") - if not file then error("Failed to open file: "..path) end + if not file then error("Failed to open file: " .. path) end local content = kernel.vfs.read(file, 1024000) kernel.vfs.close(file) return content end +local function writeFile(path, content) + local file = kernel.vfs.open(path, "w") + if not file then error("Failed to open file for writing: " .. path) end + kernel.vfs.write(file, content) + kernel.vfs.close(file) +end + local blake2s do local MOD32 = 2^32 - local function norm(x) - return x % MOD32 - end - + local function norm(x) return x % MOD32 end local function tobits(x) x = norm(x) local t = {} - for i = 0, 31 do - local b = x % 2 - t[i] = b - x = (x - b) / 2 - end + for i = 0, 31 do local b = x % 2; t[i] = b; x = (x - b) / 2 end return t end - local function frombits(t) - local x = 0 - local p = 1 - for i = 0, 31 do - if t[i] == 1 then - x = x + p - end - p = p * 2 - end + local x, p = 0, 1 + for i = 0, 31 do if t[i] == 1 then x = x + p end; p = p * 2 end return norm(x) end - local function bor(...) local args = {...} if #args == 0 then return 0 end local bits = tobits(args[1]) for i = 2, #args do local b = tobits(args[i]) - for j = 0, 31 do - bits[j] = (bits[j] == 1 or b[j] == 1) and 1 or 0 - end + for j = 0, 31 do bits[j] = (bits[j] == 1 or b[j] == 1) and 1 or 0 end end return frombits(bits) end - local function bxor(...) local args = {...} if #args == 0 then return 0 end local bits = tobits(args[1]) for i = 2, #args do local b = tobits(args[i]) - for j = 0, 31 do - bits[j] = (bits[j] ~= b[j]) and 1 or 0 - end + for j = 0, 31 do bits[j] = (bits[j] ~= b[j]) and 1 or 0 end end return frombits(bits) end - - local function lshift(x, n) - return norm(norm(x) * 2^n) - end - - local function rshift(x, n) - return math.floor(norm(x) / 2^n) - end - - local function rotr(x, n) - return bor(rshift(x, n), lshift(x, 32 - n)) - end - + local function lshift(x, n) return norm(norm(x) * 2^n) end + local function rshift(x, n) return math.floor(norm(x) / 2^n) end + local function rotr(x, n) return bor(rshift(x, n), lshift(x, 32 - n)) end local IV = { 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19 } - local SIGMA = { {0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}, {14,10,4,8,9,15,13,6,1,12,0,2,11,7,5,3}, @@ -111,7 +75,6 @@ do {6,15,14,9,11,3,0,8,12,2,13,7,1,4,10,5}, {10,2,8,4,7,6,1,5,15,11,9,14,3,12,13,0} } - local function G(v, a, b, c, d, x, y) v[a] = (v[a] + v[b] + x) % MOD32 v[d] = rotr(bxor(v[d], v[a]), 16) @@ -122,27 +85,20 @@ do v[c] = (v[c] + v[d]) % MOD32 v[b] = rotr(bxor(v[b], v[c]), 7) end - local function compress(h, block, t, last) local v = {} for i = 1, 8 do v[i] = h[i] end for i = 1, 8 do v[i + 8] = IV[i] end - v[13] = bxor(v[13], t) - if last then - v[15] = bxor(v[15], 0xFFFFFFFF) - end - + if last then v[15] = bxor(v[15], 0xFFFFFFFF) end local m = {} for i = 0, 15 do local p = i * 4 + 1 - m[i] = - (block:byte(p) or 0) + - ((block:byte(p + 1) or 0) * 0x100) + - ((block:byte(p + 2) or 0) * 0x10000) + - ((block:byte(p + 3) or 0) * 0x1000000) + m[i] = (block:byte(p) or 0) + + ((block:byte(p+1) or 0) * 0x100) + + ((block:byte(p+2) or 0) * 0x10000) + + ((block:byte(p+3) or 0) * 0x1000000) end - for r = 1, 10 do local s = SIGMA[r] G(v,1,5,9,13, m[s[1]], m[s[2]]) @@ -154,45 +110,28 @@ do G(v,3,8,9,14, m[s[13]], m[s[14]]) G(v,4,5,10,15, m[s[15]], m[s[16]]) end - - for i = 1, 8 do - h[i] = bxor(h[i], v[i], v[i + 8]) - end + for i = 1, 8 do h[i] = bxor(h[i], v[i], v[i+8]) end end - function blake2s(msg, key) key = key or "" - local h = {} for i = 1, 8 do h[i] = IV[i] end - - local outlen = 32 -- bytes - h[1] = bxor( - h[1], - 0x01010000 + lshift(#key, 8) + outlen - ) - + local outlen = 32 + h[1] = bxor(h[1], 0x01010000 + lshift(#key, 8) + outlen) local t = 0 - if #key > 0 then local block = key .. string.rep("\0", 64 - #key) t = #key compress(h, block, t, false) end - for i = 1, #msg, 64 do local block = msg:sub(i, i + 63) - if #block < 64 then - block = block .. string.rep("\0", 64 - #block) - end + if #block < 64 then block = block .. string.rep("\0", 64 - #block) end t = t + math.min(64, #msg - i + 1) compress(h, block, t, i + 64 > #msg) end - local out = "" - for i = 1, 8 do - out = out .. string.format("%08x", h[i]) - end + for i = 1, 8 do out = out .. string.format("%08x", h[i]) end return out end end @@ -202,31 +141,485 @@ if not blake2s then error("Failed to load blake2s") end if not kernel.vfs.exists("/etc/pam.d/secret") then kernel.log("PAM SECRET REGENERATING PLEASE USE ROOT") local key = "" - for i=1, 256 do - key=key..string.char(math.random(0,255)) - end + for i = 1, 256 do key = key .. string.char(math.random(0, 255)) end local handle = kernel.vfs.open("/etc/pam.d/secret", "w") kernel.vfs.write(handle, key) kernel.vfs.close(handle) end local pepper = getFile("/etc/pam.d/secret") + +local function genSalt() + local chars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789./" + local s = "" + for i = 1, 16 do + s = s .. chars:sub(math.random(1, #chars), math.random(1, #chars)) + end + return s +end + +local function hashPassword(password, salt) + local key = (pepper .. salt):sub(1, 32) + return blake2s(password, key) +end + local passwdFile = getFile("/etc/passwd") local shadowFile = getFile("/etc/shadow") -local passwdLines = string.split(passwdFile,"\n") -local shadowLines = string.split(shadowFile,"\n") -local passwd,shadow={},{} -for _,v in ipairs(passwdLines) do - passwd[#passwd+1]=string.split(v,":") +local passwdLines = string.split(passwdFile, "\n") +local shadowLines = string.split(shadowFile, "\n") + +local passwd, shadow = {}, {} +for _, v in ipairs(passwdLines) do + local fields = string.split(v, ":") + if fields[1] and fields[1] ~= "" then + passwd[#passwd + 1] = fields + end +end +for _, v in ipairs(shadowLines) do + local fields = string.split(v, ":") + if fields[1] and fields[1] ~= "" then + shadow[#shadow + 1] = fields + end end -for _,v in ipairs(shadowLines) do - shadow[#shadow+1]=string.split(v,":") +for _, v in ipairs(passwd) do + local uid = tonumber(v[1]) + if uid then kernel.users[uid] = v[3] end end -for i,v in pairs(passwd) do - kernel.users[tonumber(v[1])]=v[3] -end -kernel.passwd=passwd +kernel.passwd = passwd +local function flushPasswd() + local lines = {} + for _, v in ipairs(passwd) do + lines[#lines + 1] = table.concat(v, ":") + end + writeFile("/etc/passwd", table.concat(lines, "\n")) +end + +local function flushShadow() + local lines = {} + for _, v in ipairs(shadow) do + lines[#lines + 1] = table.concat(v, ":") + end + writeFile("/etc/shadow", table.concat(lines, "\n")) +end + +local function getPasswdByUID(uid) + for _, v in ipairs(passwd) do + if tonumber(v[1]) == uid then return v end + end + return nil +end + +local function getShadowByUID(uid) + for _, v in ipairs(shadow) do + if tonumber(v[1]) == uid then return v end + end + return nil +end + +local function getPasswdByUsername(username) + for _, v in ipairs(passwd) do + if v[3] == username then return v end + end + return nil +end + +local function nextUID() + local max = 999 + for _, v in ipairs(passwd) do + local uid = tonumber(v[1]) + if uid and uid >= 1000 and uid > max then max = uid end + end + return max + 1 +end + +function auth.login(username, password) + if type(username) ~= "string" or type(password) ~= "string" then + return nil, "Authentication failure" + end + + local entry = getPasswdByUsername(username) + if not entry then + -- timing attack resistance + hashPassword(password, "aaaaaaaaaaaaaaaa") + return nil, "Authentication failure" + end + + local uid = tonumber(entry[1]) + local sEntry = getShadowByUID(uid) + if not sEntry then + hashPassword(password, "aaaaaaaaaaaaaaaa") + return nil, "Authentication failure" + end + + local salt = sEntry[2] + local storedHash = sEntry[3] + + local computed = hashPassword(password, salt) + if computed ~= storedHash then + return nil, "Authentication failure" + end + + kernel.currentUID = uid + if kernel.currentProcess then + kernel.currentProcess.uid = uid + kernel.currentProcess.euid = uid + kernel.currentProcess.gid = tonumber(entry[2]) or uid + kernel.currentProcess.egid = tonumber(entry[2]) or uid + end + + kernel.log("AUTH: login uid=" .. tostring(uid) .. " (" .. username .. ")") + return true +end + +function auth.setPassword(uid, newPassword) + uid = tonumber(uid) + if not uid then return nil, "Invalid uid" end + + local callerUID = (kernel.currentProcess and kernel.currentProcess.euid) + or kernel.currentUID or 0 + + if callerUID ~= 0 and callerUID ~= uid then + return nil, "Permission denied" + end + + if type(newPassword) ~= "string" or #newPassword == 0 then + return nil, "Password may not be empty" + end + + if #newPassword < 6 then + return nil, "Password is too short (minimum 6 characters)" + end + + local salt = genSalt() + local hash = hashPassword(newPassword, salt) + + local sEntry = getShadowByUID(uid) + if sEntry then + sEntry[2] = salt + sEntry[3] = hash + else + shadow[#shadow + 1] = { tostring(uid), salt, hash } + end + + flushShadow() + kernel.log("AUTH: password changed for uid=" .. tostring(uid)) + return true +end + +function auth.setUsername(uid, newUsername) + uid = tonumber(uid) + if not uid then return nil, "Invalid uid" end + + local callerUID = (kernel.currentProcess and kernel.currentProcess.euid) + or kernel.currentUID or 0 + + if callerUID ~= 0 then + return nil, "Permission denied (root only)" + end + + if type(newUsername) ~= "string" or #newUsername == 0 then + return nil, "Invalid username" + end + + if not newUsername:match("^[a-z_][a-z0-9_%-]*$") or #newUsername > 32 then + return nil, "Invalid username format" + end + + if getPasswdByUsername(newUsername) then + return nil, "Username already taken" + end + + local entry = getPasswdByUID(uid) + if not entry then return nil, "No such user" end + + local oldName = entry[3] + entry[3] = newUsername + kernel.users[uid] = newUsername + + flushPasswd() + kernel.log("AUTH: uid=" .. tostring(uid) .. " renamed '" .. oldName .. "' → '" .. newUsername .. "'") + return true +end + +function auth.newUser(username, password, gid, homedir, shell) + local callerUID = (kernel.currentProcess and kernel.currentProcess.euid) + or kernel.currentUID or 0 + + if callerUID ~= 0 then + return nil, "Permission denied (root only)" + end + + if type(username) ~= "string" or #username == 0 then + return nil, "Invalid username" + end + + if not username:match("^[a-z_][a-z0-9_%-]*$") or #username > 32 then + return nil, "Invalid username format" + end + + if getPasswdByUsername(username) then + return nil, "Username already exists" + end + + if type(password) ~= "string" or #password < 6 then + return nil, "Password is too short (minimum 6 characters)" + end + + local uid = nextUID() + gid = tonumber(gid) or uid + homedir = homedir or ("/home/" .. username) + shell = shell or "/bin/sh" + + passwd[#passwd + 1] = { + tostring(uid), + tostring(gid), + username, + homedir, + shell + } + kernel.users[uid] = username + + local salt = genSalt() + local hash = hashPassword(password, salt) + shadow[#shadow + 1] = { tostring(uid), salt, hash } + + flushPasswd() + flushShadow() + + if kernel.vfs.mkdir and not kernel.vfs.exists(homedir) then + kernel.vfs.mkdir(homedir) + end + + kernel.log("AUTH: new user '" .. username .. "' uid=" .. tostring(uid)) + return uid +end + +function auth.whoami() + local uid = (kernel.currentProcess and kernel.currentProcess.euid) + or kernel.currentUID + if not uid then return nil, "Not logged in" end + return kernel.users[uid] or ("uid=" .. tostring(uid)) +end + +function auth.getUID(username) + local entry = getPasswdByUsername(username) + if entry then return tonumber(entry[1]) end + return nil +end + +function auth.getPasswd(uid) + uid = tonumber(uid) + local entry = getPasswdByUID(uid) + if not entry then return nil end + return { + uid = tonumber(entry[1]), + gid = tonumber(entry[2]), + username = entry[3], + homedir = entry[4], + shell = entry[5], + } +end + +function auth.deleteUser(uid) + uid = tonumber(uid) + if not uid then return nil, "Invalid uid" end + + local callerUID = kernel.uid or 0 + if callerUID ~= 0 then return nil, "Permission denied (root only)" end + if uid == 0 then return nil, "Cannot delete root" end + + local entry = getPasswdByUID(uid) + if not entry then return nil, "No such user" end + local username = entry[3] + + -- Remove from passwd + for i, v in ipairs(passwd) do + if tonumber(v[1]) == uid then table.remove(passwd, i); break end + end + -- Remove from shadow + for i, v in ipairs(shadow) do + if tonumber(v[1]) == uid then table.remove(shadow, i); break end + end + kernel.users[uid] = nil + + flushPasswd() + flushShadow() + kernel.log("AUTH: deleted user '" .. username .. "' uid=" .. tostring(uid)) + return true +end + +function auth.lockUser(uid) + uid = tonumber(uid) + if not uid then return nil, "Invalid uid" end + + local callerUID = kernel.uid or 0 + if callerUID ~= 0 then return nil, "Permission denied (root only)" end + if uid == 0 then return nil, "Cannot lock root" end + + local sEntry = getShadowByUID(uid) + if not sEntry then return nil, "No shadow entry for uid" end + + -- Prefix hash with ! to lock (standard Linux convention) + if sEntry[3]:sub(1,1) ~= "!" then + sEntry[3] = "!" .. sEntry[3] + end + flushShadow() + kernel.log("AUTH: locked uid=" .. tostring(uid)) + return true +end + +function auth.unlockUser(uid) + uid = tonumber(uid) + if not uid then return nil, "Invalid uid" end + + local callerUID = kernel.uid or 0 + if callerUID ~= 0 then return nil, "Permission denied (root only)" end + + local sEntry = getShadowByUID(uid) + if not sEntry then return nil, "No shadow entry for uid" end + + if sEntry[3]:sub(1,1) == "!" then + sEntry[3] = sEntry[3]:sub(2) + end + flushShadow() + kernel.log("AUTH: unlocked uid=" .. tostring(uid)) + return true +end + +function auth.listUsers() + local result = {} + for _, v in ipairs(passwd) do + local uid = tonumber(v[1]) + local sEntry = getShadowByUID(uid) + local locked = sEntry and sEntry[3]:sub(1,1) == "!" + result[#result+1] = { + uid = uid, + gid = tonumber(v[2]), + username = v[3], + homedir = v[4], + shell = v[5], + locked = locked or false, + } + end + return result +end + +function auth.setShell(uid, shell) + uid = tonumber(uid) + if not uid then return nil, "Invalid uid" end + + local callerUID = kernel.uid or 0 + if callerUID ~= 0 and callerUID ~= uid then + return nil, "Permission denied" + end + + if type(shell) ~= "string" or #shell == 0 then + return nil, "Invalid shell" + end + + local entry = getPasswdByUID(uid) + if not entry then return nil, "No such user" end + entry[5] = shell + + flushPasswd() + kernel.log("AUTH: uid=" .. tostring(uid) .. " shell -> " .. shell) + return true +end + +function auth.setHomedir(uid, homedir) + uid = tonumber(uid) + if not uid then return nil, "Invalid uid" end + + local callerUID = kernel.uid or 0 + if callerUID ~= 0 then return nil, "Permission denied (root only)" end + + if type(homedir) ~= "string" or #homedir == 0 then + return nil, "Invalid homedir" + end + + local entry = getPasswdByUID(uid) + if not entry then return nil, "No such user" end + entry[4] = homedir + + flushPasswd() + kernel.log("AUTH: uid=" .. tostring(uid) .. " homedir -> " .. homedir) + return true +end + +function auth.setGID(uid, gid) + uid = tonumber(uid) + gid = tonumber(gid) + if not uid or not gid then return nil, "Invalid uid or gid" end + + local callerUID = kernel.uid or 0 + if callerUID ~= 0 then return nil, "Permission denied (root only)" end + + local entry = getPasswdByUID(uid) + if not entry then return nil, "No such user" end + entry[2] = tostring(gid) + + flushPasswd() + kernel.log("AUTH: uid=" .. tostring(uid) .. " gid -> " .. tostring(gid)) + return true +end + +-- Elevate the calling task to targetUid after verifying targetUsername's password. +-- This is the kernel-side primitive for su/sudo — it bypasses the kernel.uid==0 +-- check in sys.setuid because the auth module itself is trusted kernel code. +function auth.elevate(targetUsername, password) + if type(targetUsername) ~= "string" or type(password) ~= "string" then + return nil, "Authentication failure" + end + + local entry = getPasswdByUsername(targetUsername) + if not entry then + hashPassword(password, "aaaaaaaaaaaaaaaa") -- timing resistance + return nil, "Authentication failure" + end + + local uid = tonumber(entry[1]) + local sEntry = getShadowByUID(uid) + if not sEntry then + hashPassword(password, "aaaaaaaaaaaaaaaa") + return nil, "Authentication failure" + end + + local computed = hashPassword(password, sEntry[2]) + if computed ~= sEntry[3] then + return nil, "Authentication failure" + end + + -- Directly set the calling task's uid — trusted kernel path + local task = kernel.currentTask + local prevUid = task.uid + task.uid = uid + task.euid = uid + task.gid = tonumber(entry[2]) or uid + task.egid = tonumber(entry[2]) or uid + kernel.uid = uid + + kernel.log("AUTH: elevate uid=" .. tostring(prevUid) .. " -> " .. tostring(uid) .. " (" .. targetUsername .. ")") + return true, uid +end + +if kernel.syscalls then + kernel.syscalls["auth_login"] = auth.login + kernel.syscalls["auth_setpassword"] = auth.setPassword + kernel.syscalls["auth_setusername"] = auth.setUsername + kernel.syscalls["auth_newuser"] = auth.newUser + kernel.syscalls["auth_whoami"] = auth.whoami + kernel.syscalls["auth_getuid"] = auth.getUID + kernel.syscalls["auth_getpasswd"] = auth.getPasswd + kernel.syscalls["auth_elevate"] = auth.elevate + kernel.syscalls["auth_deleteuser"] = auth.deleteUser + kernel.syscalls["auth_lockuser"] = auth.lockUser + kernel.syscalls["auth_unlockuser"] = auth.unlockUser + kernel.syscalls["auth_listusers"] = auth.listUsers + kernel.syscalls["auth_setshell"] = auth.setShell + kernel.syscalls["auth_sethomedir"] = auth.setHomedir + kernel.syscalls["auth_setgid"] = auth.setGID +end diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/91_login.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/91_login.kmod new file mode 100644 index 0000000..7835c95 --- /dev/null +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/91_login.kmod @@ -0,0 +1,16 @@ +--:Minify:-- +local kernel = ... + +-- It runs at uid 0 so it can call setuid() to drop privileges to the logged in user +kernel.processes.login = function() + local handle = kernel.vfs.open("/bin/login", "r") + local text = kernel.vfs.read(handle, 1024 * 1024) + kernel.vfs.close(handle) + + local fn, err = load(text, "@/bin/login", "t", kernel._U) + if not fn then + kernel.log("Failed to load /bin/login: " .. tostring(err), "ERROR", 2) + return + end + fn() +end diff --git a/Src/Hyperion-kernel/lib/modules/Hyperion/92_permissions.kmod b/Src/Hyperion-kernel/lib/modules/Hyperion/92_permissions.kmod new file mode 100644 index 0000000..2418746 --- /dev/null +++ b/Src/Hyperion-kernel/lib/modules/Hyperion/92_permissions.kmod @@ -0,0 +1,165 @@ +--:Minify:-- +local kernel = ... + +local bit32 = require("bit32") +local bor = bit32.bor +local lshift = bit32.lshift + +-- bit 0 = everyone-write, bit 1 = everyone-read +-- bit 2 = group-write, bit 3 = group-read +-- bit 4 = owner-write, bit 5 = owner-read +-- bit 6 = suid +local P_OWNER_R = lshift(1, 5) +local P_OWNER_W = lshift(1, 4) +local P_GROUP_R = lshift(1, 3) +local P_GROUP_W = lshift(1, 2) +local P_WORLD_R = lshift(1, 1) +local P_WORLD_W = lshift(1, 0) +local P_SUID = lshift(1, 6) + +local RW_R_R = bor(P_OWNER_R, P_OWNER_W, P_GROUP_R, P_WORLD_R) -- 644 / rw-r--r-- +local RWX_R_R = bor(P_OWNER_R, P_OWNER_W, P_GROUP_R, P_WORLD_R) -- 755 / rwxr--r-- +local RW_R__ = bor(P_OWNER_R, P_OWNER_W, P_GROUP_R) -- 640 / rw-r----- +local RW____ = bor(P_OWNER_R, P_OWNER_W) -- 600 / rw------- +local SUID_755 = bor(P_SUID, P_OWNER_R, P_OWNER_W, P_GROUP_R, P_WORLD_R) -- 4755 + +local function metaEntry(name, owner, group, perms) + return string.char(#name) .. name + .. string.char(owner, group, perms) + .. string.char(0) +end + +local rootDisk = kernel.disks["$"] + +local function writeMeta(dir, entries) + local diskDir = dir == "/" and "/" or dir + local path = (diskDir:sub(-1) == "/" and diskDir or diskDir .. "/") .. ".meta" + if path:sub(1,1) == "/" then path = path:sub(2) end + if path == "" then path = ".meta" end + + local data = "" + for _, e in ipairs(entries) do + data = data .. metaEntry(e[1], e[2], e[3], e[4]) + end + + local ok, err = pcall(function() + local f = rootDisk:open(path, "w") + f.write(data) + f.close() + end) + if not ok then + kernel.log("permissions: failed to write /" .. path .. ": " .. tostring(err), "WARN", 8) + end +end + +if rootDisk:fileExists(".meta") then + kernel.log("Permissions already seeded, skipping.", "INFO") +else + kernel.log("Seeding filesystem permissions...", "INFO") + + writeMeta("/", { + {"bin", 0, 0, RWX_R_R}, + {"boot", 0, 0, RWX_R_R}, + {"dev", 0, 0, RWX_R_R}, + {"etc", 0, 0, RWX_R_R}, + {"home", 0, 0, RWX_R_R}, + {"lib", 0, 0, RWX_R_R}, + {"root", 0, 0, RW____ }, + {"sbin", 0, 0, RWX_R_R}, + {"tmp", 0, 0, bor(P_OWNER_R, P_OWNER_W, P_GROUP_R, P_GROUP_W, P_WORLD_R, P_WORLD_W)}, + {"usr", 0, 0, RWX_R_R}, + {"var", 0, 0, RWX_R_R}, + }) + + writeMeta("/bin", { + {"cat", 0, 0, RWX_R_R}, + {"clear", 0, 0, RWX_R_R}, + {"echo", 0, 0, RWX_R_R}, + {"hfetch", 0, 0, RWX_R_R}, + {"hysh", 0, 0, RWX_R_R}, + {"hyshex", 0, 0, RWX_R_R}, + {"install", 0, 0, RWX_R_R}, + {"login", 0, 0, SUID_755}, + {"ls", 0, 0, RWX_R_R}, + {"lua", 0, 0, RWX_R_R}, + {"luaold", 0, 0, RWX_R_R}, + {"mkdir", 0, 0, RWX_R_R}, + {"ps", 0, 0, RWX_R_R}, + {"pwd", 0, 0, RWX_R_R}, + {"spm", 0, 0, RWX_R_R}, + {"su", 0, 0, SUID_755}, + {"sudo", 0, 0, SUID_755}, + {"sysdump", 0, 0, RWX_R_R}, + {"whoami", 0, 0, RWX_R_R}, + {"yes", 0, 0, RWX_R_R}, + {"startup", 0, 0, RWX_R_R}, + }) + + writeMeta("/bin/startup", { + {"test.lua", 0, 0, RWX_R_R}, + }) + + writeMeta("/etc", { + {"passwd", 0, 0, RW_R_R}, + {"shadow", 0, 0, RW____ }, + {"pam.d", 0, 0, RWX_R_R}, + }) + + writeMeta("/etc/pam.d", { + {"secret", 0, 0, RW____}, + }) + + writeMeta("/sbin", { + {"init.lua", 0, 0, RWX_R_R}, + }) + + writeMeta("/boot", { + {"kernel.lua", 0, 0, RW_R_R}, + {"boot.cfg", 0, 0, RW_R_R}, + {"safeboot.cfg", 0, 0, RW_R_R}, + {"fstab", 0, 0, RW_R_R}, + {"initfs", 0, 0, RW_R_R}, + {"cct", 0, 0, RWX_R_R}, + {"oc", 0, 0, RWX_R_R}, + }) + + writeMeta("/lib", { + {"sys", 0, 0, RWX_R_R}, + {"modules", 0, 0, RWX_R_R}, + {"crypto", 0, 0, RWX_R_R}, + {"store", 0, 0, RWX_R_R}, + {"snip", 0, 0, RW_R_R}, + {"io", 0, 0, RW_R_R}, + {"bit32", 0, 0, RW_R_R}, + }) + + kernel.log("Filesystem permissions seeded.", "INFO") +end + +-- TODO: move this to vfs.kmod +local _orig_open = kernel.vfs.open +kernel.vfs.open = function(path, mode) + local fd = _orig_open(path, mode) + if mode == "r" then + local task = kernel.currentTask + local fobj = task.fd[fd] + if fobj and fobj.meta then + local suid_set = bit32.extract(fobj.meta.perms, 6) == 1 + if suid_set then + fobj.suid_owner = fobj.meta.owner + end + end + end + return fd +end + +kernel.syscalls["fget_suid"] = function(fd) + local task = kernel.currentTask + local fobj = task and task.fd[fd] + if fobj and fobj.suid_owner then + return fobj.suid_owner + end + return nil +end + +kernel.log("Permission module loaded.", "INFO")