added users (spsf untangled by astronand)

This commit is contained in:
2026-02-20 23:32:05 -05:00
parent 57b1d46837
commit 0655f2a39e
20 changed files with 2419 additions and 237 deletions

292
Src/Hyperion-bash/bin/help Normal file
View File

@@ -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 <dir>",
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] <command> [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] <username>",
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] <username>",
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] <username>",
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 <command>
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 <command>' 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)

View File

@@ -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

299
Src/Hyperion-bash/bin/hysh Normal file
View File

@@ -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

View File

@@ -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

19
Src/Hyperion-bash/bin/id Normal file
View File

@@ -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))

178
Src/Hyperion-bash/bin/login Normal file
View File

@@ -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

View File

@@ -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

View File

@@ -1,149 +1,325 @@
syscall.devctl(1,"sfgc",7) --:Minify:--
print("HyperionOS Lua prompt.") local C_PROMPT = 7
print("Call exit() to exit.") 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 function c(col) syscall.devctl(1, "sfgc", col) end
local luaEnv=setmetatable({ local function w(s) syscall.write(1, tostring(s)) end
["exit"] = setmetatable({}, {
__tostring = function() return "Call exit() to exit." end,
__call = function() syscall.exit() end,
}),
["_echo"] = function(...)
return ...
end,
},{__index=_ENV})
local function getUserInput() local MAX_DEPTH = 6
syscall.devctl(1,"sfgc",1) local MAX_ENTRIES = 64
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 input = "" local function prettyVal(val, indent, seen)
local blinkState = false indent = indent or 0
local cursorPos = 1 seen = seen or {}
local history = 0 local t = type(val)
while true do if t == "nil" then
local key=syscall.read(0) c(C_NIL); w("nil")
if key then elseif t == "boolean" then
if key == "\19" then --TODO: REPLACE WITH LEFT ARROW c(C_BOOL); w(tostring(val))
if cursorPos > 1 then elseif t == "number" then
cursorPos = cursorPos - 1 c(C_NUM)
end if val ~= val then
elseif key == "\20" then --TODO: REPLACE WITH RIGHT ARROW w("nan")
if cursorPos <= #input then elseif val == math.huge then
cursorPos = cursorPos + 1 w("inf")
end elseif val == -math.huge then
elseif key == "\17" then --TODO: REPLACE WITH UP ARROW w("-inf")
if history < #commandHistory then elseif val == math.floor(val) and math.abs(val) < 1e15 then
syscall.devctl(1,"spos",curOffsetX,curOffsetY) w(tostring(math.floor(val)))
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(" ")
else else
printInline(string.sub(input, cursorPos, cursorPos)) w(tostring(val))
end end
syscall.devctl(1,"sfgc",1) elseif t == "string" then
syscall.devctl(1,"sbgc",16) c(C_STR)
printInline(string.sub(input, cursorPos+1)) local s = string.format("%q", val)
if cursorPos <= #input then w(s)
printInline(" ") elseif t == "function" then
c(C_TABLE); w(tostring(val))
elseif t == "table" then
if seen[val] then
c(C_TABLE); w("<circular " .. tostring(val) .. ">")
return
end end
local curBlink = ((math.floor(syscall.getUptime() / 500) % 2) == 0) if indent >= MAX_DEPTH then
if curBlink ~= blinkState then c(C_TABLE); w("<table " .. tostring(val) .. ">")
blinkState = curBlink return
end 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 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("<eof>") ~= 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 end
local function runCode(code) local function runCode(code)
local func, err = load(code, "@lua", "t", luaEnv) local fn, isExpr, err = compile(code)
local isReturn = false if not fn then
if load("return "..code) then c(C_ERR); w("[error] "); c(1); w(cleanErr(err) .. "\n")
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))
return return
end end
local success, msg = xpcall(func, debug.traceback) local results = table.pack(xpcall(fn, debug.traceback))
if not success then local ok = table.remove(results, 1)
local errSL = string.sub(msg, string.find(msg, ":") + 1) results.n = results.n - 1
syscall.devctl(1,"sfgc",2)
printInline("@lua: Runtime error on line ") if not ok then
print(string.sub(errSL, 1, string.find(errSL, ":") - 1)) c(C_ERR); w("[error] "); c(1); w(cleanErr(results[1]) .. "\n")
syscall.devctl(1,"sfgc",1) elseif isExpr and results.n > 0 then
print(string.sub(errSL, string.find(errSL, ":") + 1)) c(C_OUT); w("= ")
elseif isReturn then printResults(table.unpack(results, 1, results.n))
print(tostring(msg))
end end
end end
while true do local function getUserInput(prompt, history)
local code = getUserInput() c(C_PROMPT); w(prompt); c(1)
if code ~= "" then local pos = syscall.devctl(1, "gpos")
if code ~= commandHistory[#commandHistory] then local ox = tonumber(pos:sub(1, pos:find(";")-1))
table.insert(commandHistory, code) 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
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 end
runCode(code) 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
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

View File

@@ -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 .. "'")

72
Src/Hyperion-bash/bin/su Normal file
View File

@@ -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()

110
Src/Hyperion-bash/bin/sudo Normal file
View File

@@ -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] <command> [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

View File

@@ -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] <username>")
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))

View File

@@ -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] <username>")
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 .. "'")

View File

@@ -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] <username>")
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 .. "'")

View File

@@ -1,5 +1,4 @@
--:Minify:-- --:Minify:--
sleep(1)
local BOOT_DRIVE_PATH=({...})[1] or "/$" local BOOT_DRIVE_PATH=({...})[1] or "/$"
-- UnBIOS by JackMacWindows -- 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 -- This will undo most of the changes/additions made in the BIOS, but some things may remain wrapped if `debug` is unavailable

View File

@@ -17,6 +17,7 @@ function string.delim(str, ...) return table.concat(table.pack(...), str) end
function string.split(str, delim, maxResultCountOrNil) function string.split(str, delim, maxResultCountOrNil)
assert(#delim == 1, "only delim len 1 supported for now") assert(#delim == 1, "only delim len 1 supported for now")
if not str then return false end
maxResultCountOrNil = (maxResultCountOrNil or 0) - 1 maxResultCountOrNil = (maxResultCountOrNil or 0) - 1
local rv = {} local rv = {}
local buf = "" local buf = ""

View File

@@ -334,7 +334,7 @@ function vfs.read(fd, count)
local file = task.fd[fd] local file = task.fd[fd]
if not file then error("EBADF") end if not file then error("EBADF") end
if not file.handle.read 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 end
-- Write -- Write
@@ -354,7 +354,7 @@ function vfs.pread(fd, count, offset)
if not file.handle.read then error("EBADF") end if not file.handle.read then error("EBADF") end
if not file.handle.seek then error("EBADF") end if not file.handle.seek then error("EBADF") end
file.handle.seek("set", offset) file.handle.seek("set", offset)
return file.handle.read(count or 1) return file.handle.read(count or 1) or ""
end end
function vfs.pwrite(fd, content, offset) function vfs.pwrite(fd, content, offset)

View File

@@ -3,102 +3,66 @@ local kernel = ...
local auth = {} local auth = {}
kernel.auth = auth kernel.auth = auth
-- @SPSF work here -- PASSWD FILE FORMAT: uid:gid:username:homedir:shell
-- SHADOW FILE FORMAT: uid:salt:hash
-- 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
local function getFile(path) local function getFile(path)
local file = kernel.vfs.open(path, "r") 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) local content = kernel.vfs.read(file, 1024000)
kernel.vfs.close(file) kernel.vfs.close(file)
return content return content
end 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 local blake2s
do do
local MOD32 = 2^32 local MOD32 = 2^32
local function norm(x) local function norm(x) return x % MOD32 end
return x % MOD32
end
local function tobits(x) local function tobits(x)
x = norm(x) x = norm(x)
local t = {} local t = {}
for i = 0, 31 do for i = 0, 31 do local b = x % 2; t[i] = b; x = (x - b) / 2 end
local b = x % 2
t[i] = b
x = (x - b) / 2
end
return t return t
end end
local function frombits(t) local function frombits(t)
local x = 0 local x, p = 0, 1
local p = 1 for i = 0, 31 do if t[i] == 1 then x = x + p end; p = p * 2 end
for i = 0, 31 do
if t[i] == 1 then
x = x + p
end
p = p * 2
end
return norm(x) return norm(x)
end end
local function bor(...) local function bor(...)
local args = {...} local args = {...}
if #args == 0 then return 0 end if #args == 0 then return 0 end
local bits = tobits(args[1]) local bits = tobits(args[1])
for i = 2, #args do for i = 2, #args do
local b = tobits(args[i]) local b = tobits(args[i])
for j = 0, 31 do for j = 0, 31 do bits[j] = (bits[j] == 1 or b[j] == 1) and 1 or 0 end
bits[j] = (bits[j] == 1 or b[j] == 1) and 1 or 0
end
end end
return frombits(bits) return frombits(bits)
end end
local function bxor(...) local function bxor(...)
local args = {...} local args = {...}
if #args == 0 then return 0 end if #args == 0 then return 0 end
local bits = tobits(args[1]) local bits = tobits(args[1])
for i = 2, #args do for i = 2, #args do
local b = tobits(args[i]) local b = tobits(args[i])
for j = 0, 31 do for j = 0, 31 do bits[j] = (bits[j] ~= b[j]) and 1 or 0 end
bits[j] = (bits[j] ~= b[j]) and 1 or 0
end
end end
return frombits(bits) return frombits(bits)
end end
local function lshift(x, n) return norm(norm(x) * 2^n) end
local function lshift(x, n) local function rshift(x, n) return math.floor(norm(x) / 2^n) end
return norm(norm(x) * 2^n) local function rotr(x, n) return bor(rshift(x, n), lshift(x, 32 - n)) end
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 = { local IV = {
0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A, 0x6A09E667, 0xBB67AE85, 0x3C6EF372, 0xA54FF53A,
0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19 0x510E527F, 0x9B05688C, 0x1F83D9AB, 0x5BE0CD19
} }
local SIGMA = { local SIGMA = {
{0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15}, {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}, {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}, {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} {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) local function G(v, a, b, c, d, x, y)
v[a] = (v[a] + v[b] + x) % MOD32 v[a] = (v[a] + v[b] + x) % MOD32
v[d] = rotr(bxor(v[d], v[a]), 16) v[d] = rotr(bxor(v[d], v[a]), 16)
@@ -122,27 +85,20 @@ do
v[c] = (v[c] + v[d]) % MOD32 v[c] = (v[c] + v[d]) % MOD32
v[b] = rotr(bxor(v[b], v[c]), 7) v[b] = rotr(bxor(v[b], v[c]), 7)
end end
local function compress(h, block, t, last) local function compress(h, block, t, last)
local v = {} local v = {}
for i = 1, 8 do v[i] = h[i] end for i = 1, 8 do v[i] = h[i] end
for i = 1, 8 do v[i + 8] = IV[i] end for i = 1, 8 do v[i + 8] = IV[i] end
v[13] = bxor(v[13], t) v[13] = bxor(v[13], t)
if last then if last then v[15] = bxor(v[15], 0xFFFFFFFF) end
v[15] = bxor(v[15], 0xFFFFFFFF)
end
local m = {} local m = {}
for i = 0, 15 do for i = 0, 15 do
local p = i * 4 + 1 local p = i * 4 + 1
m[i] = m[i] = (block:byte(p) or 0)
(block:byte(p) or 0) + + ((block:byte(p+1) or 0) * 0x100)
((block:byte(p + 1) or 0) * 0x100) + + ((block:byte(p+2) or 0) * 0x10000)
((block:byte(p + 2) or 0) * 0x10000) + + ((block:byte(p+3) or 0) * 0x1000000)
((block:byte(p + 3) or 0) * 0x1000000)
end end
for r = 1, 10 do for r = 1, 10 do
local s = SIGMA[r] local s = SIGMA[r]
G(v,1,5,9,13, m[s[1]], m[s[2]]) 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,3,8,9,14, m[s[13]], m[s[14]])
G(v,4,5,10,15, m[s[15]], m[s[16]]) G(v,4,5,10,15, m[s[15]], m[s[16]])
end 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 end
function blake2s(msg, key) function blake2s(msg, key)
key = key or "" key = key or ""
local h = {} local h = {}
for i = 1, 8 do h[i] = IV[i] end for i = 1, 8 do h[i] = IV[i] end
local outlen = 32
local outlen = 32 -- bytes h[1] = bxor(h[1], 0x01010000 + lshift(#key, 8) + outlen)
h[1] = bxor(
h[1],
0x01010000 + lshift(#key, 8) + outlen
)
local t = 0 local t = 0
if #key > 0 then if #key > 0 then
local block = key .. string.rep("\0", 64 - #key) local block = key .. string.rep("\0", 64 - #key)
t = #key t = #key
compress(h, block, t, false) compress(h, block, t, false)
end end
for i = 1, #msg, 64 do for i = 1, #msg, 64 do
local block = msg:sub(i, i + 63) local block = msg:sub(i, i + 63)
if #block < 64 then if #block < 64 then block = block .. string.rep("\0", 64 - #block) end
block = block .. string.rep("\0", 64 - #block)
end
t = t + math.min(64, #msg - i + 1) t = t + math.min(64, #msg - i + 1)
compress(h, block, t, i + 64 > #msg) compress(h, block, t, i + 64 > #msg)
end end
local out = "" local out = ""
for i = 1, 8 do for i = 1, 8 do out = out .. string.format("%08x", h[i]) end
out = out .. string.format("%08x", h[i])
end
return out return out
end end
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 if not kernel.vfs.exists("/etc/pam.d/secret") then
kernel.log("PAM SECRET REGENERATING PLEASE USE ROOT") kernel.log("PAM SECRET REGENERATING PLEASE USE ROOT")
local key = "" local key = ""
for i=1, 256 do for i = 1, 256 do key = key .. string.char(math.random(0, 255)) end
key=key..string.char(math.random(0,255))
end
local handle = kernel.vfs.open("/etc/pam.d/secret", "w") local handle = kernel.vfs.open("/etc/pam.d/secret", "w")
kernel.vfs.write(handle, key) kernel.vfs.write(handle, key)
kernel.vfs.close(handle) kernel.vfs.close(handle)
end end
local pepper = getFile("/etc/pam.d/secret") 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 passwdFile = getFile("/etc/passwd")
local shadowFile = getFile("/etc/shadow") 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 local passwdLines = string.split(passwdFile, "\n")
passwd[#passwd+1]=string.split(v,":") 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 end
for _,v in ipairs(shadowLines) do for _, v in ipairs(passwd) do
shadow[#shadow+1]=string.split(v,":") local uid = tonumber(v[1])
if uid then kernel.users[uid] = v[3] end
end end
for i,v in pairs(passwd) do kernel.passwd = passwd
kernel.users[tonumber(v[1])]=v[3]
end
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

View File

@@ -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

View File

@@ -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")