init.lua (16543B)
1 -- Administrative Password Prompt 2 -- Provides secure password verification for permission and path requests 3 4 -- Base64 encoding/decoding 5 local base64 = {} 6 local b64chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' 7 8 function base64.encode(data) 9 if not data or #data == 0 then return "" end 10 local result = {} 11 local padding = (3 - bit.band(#data, 3)) % 3 12 13 for i = 1, #data, 3 do 14 local b1 = string.byte(data, i) 15 local b2 = string.byte(data, i + 1) or 0 16 local b3 = string.byte(data, i + 2) or 0 17 18 local n = b1 * 65536 + b2 * 256 + b3 19 20 local c1 = bit.band(bit.rshift(n, 18), 63) + 1 21 local c2 = bit.band(bit.rshift(n, 12), 63) + 1 22 local c3 = bit.band(bit.rshift(n, 6), 63) + 1 23 local c4 = bit.band(n, 63) + 1 24 25 table.insert(result, b64chars:sub(c1, c1)) 26 table.insert(result, b64chars:sub(c2, c2)) 27 28 if i + 1 <= #data then 29 table.insert(result, b64chars:sub(c3, c3)) 30 else 31 table.insert(result, '=') 32 end 33 34 if i + 2 <= #data then 35 table.insert(result, b64chars:sub(c4, c4)) 36 else 37 table.insert(result, '=') 38 end 39 end 40 41 return table.concat(result) 42 end 43 44 function base64.decode(data) 45 if not data then return nil end 46 data = data:gsub("%s+", "") 47 if #data == 0 then return "" end 48 if bit.band(#data, 3) ~= 0 then return nil, "Invalid base64 string length" end 49 50 local decode_table = {} 51 for i = 1, 64 do 52 decode_table[b64chars:sub(i, i)] = i - 1 53 end 54 decode_table['='] = 0 55 56 local result = {} 57 for i = 1, #data, 4 do 58 local c1 = decode_table[data:sub(i, i)] 59 local c2 = decode_table[data:sub(i + 1, i + 1)] 60 local c3 = decode_table[data:sub(i + 2, i + 2)] 61 local c4 = decode_table[data:sub(i + 3, i + 3)] 62 63 if not c1 or not c2 or not c3 or not c4 then 64 return nil, "Invalid base64 character" 65 end 66 67 local n = c1 * 262144 + c2 * 4096 + c3 * 64 + c4 68 69 local b1 = bit.band(bit.rshift(n, 16), 255) 70 local b2 = bit.band(bit.rshift(n, 8), 255) 71 local b3 = bit.band(n, 255) 72 73 table.insert(result, string.char(b1)) 74 75 if data:sub(i + 2, i + 2) ~= '=' then 76 table.insert(result, string.char(b2)) 77 end 78 79 if data:sub(i + 3, i + 3) ~= '=' then 80 table.insert(result, string.char(b3)) 81 end 82 end 83 84 return table.concat(result) 85 end 86 87 -- Password prompt state 88 local promptWindow = nil 89 local currentPassword = "" 90 local currentCallback = nil 91 local currentRequest = { 92 type = nil, -- "permission" or "path" 93 pid = nil, 94 value = nil 95 } 96 97 -- Colors 98 local COLOR_BACKGROUND = 0x2C2C2C 99 local COLOR_BORDER = 0x4A4A4A 100 local COLOR_TEXT = 0xFFFFFF 101 local COLOR_INPUT_BG = 0x1E1E1E 102 local COLOR_INPUT_BORDER = 0x5A5A5A 103 local COLOR_BUTTON_OK = 0x007ACC 104 local COLOR_BUTTON_CANCEL = 0x5A5A5A 105 local COLOR_ERROR = 0xFF4444 106 107 -- Layout constants 108 local WINDOW_WIDTH = 500 109 local WINDOW_HEIGHT = 250 110 local PADDING = 20 111 local INPUT_HEIGHT = 40 112 local BUTTON_WIDTH = 100 113 local BUTTON_HEIGHT = 35 114 115 -- UI state 116 local errorMessage = nil 117 local cursorVisible = true 118 local lastBlinkTime = os.clock() 119 120 -- Hash password with Argon2id 121 local function hashPassword(password) 122 if not crypto or not crypto.Argon2id then 123 osprint("ERROR: crypto.Argon2id not available\n") 124 return nil 125 end 126 127 -- Use a fixed salt for password verification (since we're comparing hashes) 128 -- In production, this should be a random salt stored with the hash 129 local salt = "LuajitOS-AdminSalt-v1" 130 131 local hash = crypto.Argon2id(password, salt) 132 if not hash then 133 osprint("ERROR: Argon2id hashing failed\n") 134 return nil 135 end 136 137 return hash 138 end 139 140 -- Verify password against stored hash 141 local function verifyPassword(password) 142 -- Hash the entered password 143 local attemptHash = hashPassword(password) 144 if not attemptHash then 145 errorMessage = "Hashing failed" 146 return false 147 end 148 149 -- Encode to base64 150 local attemptHashB64 = base64.encode(attemptHash) 151 152 -- Print the hash for debugging 153 osprint("Password attempt hash (base64): " .. attemptHashB64 .. "\n") 154 155 -- Read stored hash from /os/password.hash 156 local storedHashB64, err = fs:read("/os/password.hash") 157 if not storedHashB64 then 158 osprint("ERROR: Failed to read /os/password.hash: " .. tostring(err) .. "\n") 159 errorMessage = "Password file not found" 160 return false 161 end 162 163 -- Trim whitespace 164 storedHashB64 = storedHashB64:match("^%s*(.-)%s*$") 165 attemptHashB64 = attemptHashB64:match("^%s*(.-)%s*$") 166 167 osprint("Stored hash (base64): " .. storedHashB64 .. "\n") 168 osprint("Comparing hashes...\n") 169 170 -- Compare hashes 171 if attemptHashB64 == storedHashB64 then 172 osprint("Password verification: SUCCESS\n") 173 return true 174 else 175 osprint("Password verification: FAILED\n") 176 errorMessage = "Incorrect password" 177 return false 178 end 179 end 180 181 -- Handle password submission 182 local function submitPassword() 183 if #currentPassword == 0 then 184 errorMessage = "Please enter a password" 185 return 186 end 187 188 errorMessage = nil 189 190 -- Verify password 191 if verifyPassword(currentPassword) then 192 -- Password correct - execute the requested action 193 local success = false 194 195 if currentRequest.type == "permission" then 196 osprint("Granting permission '" .. currentRequest.value .. "' to PID " .. currentRequest.pid .. "\n") 197 local ok, err = pcall(function() 198 sys.ADMIN_AppAddPermission(currentRequest.pid, currentRequest.value) 199 end) 200 if not ok then 201 osprint("ERROR: Failed to add permission: " .. tostring(err) .. "\n") 202 errorMessage = "Failed to add permission" 203 else 204 success = true 205 end 206 elseif currentRequest.type == "path" then 207 osprint("Granting path '" .. currentRequest.value .. "' to PID " .. currentRequest.pid .. "\n") 208 local ok, err = pcall(function() 209 sys.ADMIN_AppAddPath(currentRequest.pid, currentRequest.value) 210 end) 211 if not ok then 212 osprint("ERROR: Failed to add path: " .. tostring(err) .. "\n") 213 errorMessage = "Failed to add path" 214 else 215 success = true 216 end 217 end 218 219 -- Call callback if successful 220 if success and currentCallback then 221 pcall(currentCallback, true) 222 end 223 224 -- Close window 225 if promptWindow and success then 226 promptWindow:hide() 227 currentPassword = "" 228 currentCallback = nil 229 currentRequest = {type = nil, pid = nil, value = nil} 230 end 231 else 232 -- Password incorrect - error message already set by verifyPassword 233 currentPassword = "" 234 end 235 end 236 237 -- Cancel prompt 238 local function cancelPrompt() 239 if currentCallback then 240 pcall(currentCallback, false) 241 end 242 243 if promptWindow then 244 promptWindow:hide() 245 end 246 247 currentPassword = "" 248 currentCallback = nil 249 currentRequest = {type = nil, pid = nil, value = nil} 250 errorMessage = nil 251 end 252 253 -- Create the password prompt window 254 local function createPromptWindow() 255 -- Center on screen 256 local screenWidth = (sys and sys.screen and sys.screen[1] and sys.screen[1].width) or 1024 257 local screenHeight = (sys and sys.screen and sys.screen[1] and sys.screen[1].height) or 768 258 local x = math.floor((screenWidth - WINDOW_WIDTH) / 2) 259 local y = math.floor((screenHeight - WINDOW_HEIGHT) / 2) 260 261 promptWindow = app:newWindow(x, y, WINDOW_WIDTH, WINDOW_HEIGHT) 262 promptWindow.title = "Administrator Authentication" 263 promptWindow.resizable = false 264 promptWindow.alwaysOnTop = true -- Draw on top of everything 265 promptWindow.noTaskbar = true -- Don't show in taskbar 266 promptWindow:hide() -- Initially hidden 267 268 -- Draw function 269 promptWindow.onDraw = function(gfx) 270 -- Background 271 gfx:fillRect(0, 0, WINDOW_WIDTH, WINDOW_HEIGHT, COLOR_BACKGROUND) 272 273 -- Title text 274 local titleY = PADDING 275 gfx:drawText(PADDING, titleY, "Administrator Access Required", COLOR_TEXT) 276 277 -- Request description 278 local descY = titleY + 25 279 if currentRequest.type == "permission" then 280 gfx:drawText(PADDING, descY, "PID " .. currentRequest.pid .. " requests permission:", COLOR_TEXT) 281 gfx:drawText(PADDING, descY + 20, "\"" .. currentRequest.value .. "\"", 0x00AAFF) 282 elseif currentRequest.type == "path" then 283 gfx:drawText(PADDING, descY, "PID " .. currentRequest.pid .. " requests path access:", COLOR_TEXT) 284 gfx:drawText(PADDING, descY + 20, "\"" .. currentRequest.value .. "\"", 0x00AAFF) 285 end 286 287 -- Password label 288 local labelY = descY + 60 289 gfx:drawText(PADDING, labelY, "Enter administrator password:", COLOR_TEXT) 290 291 -- Password input box 292 local inputY = labelY + 25 293 gfx:fillRect(PADDING, inputY, WINDOW_WIDTH - (PADDING * 2), INPUT_HEIGHT, COLOR_INPUT_BG) 294 gfx:drawRect(PADDING, inputY, WINDOW_WIDTH - (PADDING * 2), INPUT_HEIGHT, COLOR_INPUT_BORDER) 295 296 -- Password text (masked) 297 local maskedPassword = string.rep("*", #currentPassword) 298 299 -- Show cursor 300 if cursorVisible then 301 maskedPassword = maskedPassword .. "|" 302 end 303 304 gfx:drawText(PADDING + 10, inputY + 12, maskedPassword, COLOR_TEXT) 305 306 -- Error message 307 if errorMessage then 308 local errorY = inputY + INPUT_HEIGHT + 10 309 gfx:drawText(PADDING, errorY, errorMessage, COLOR_ERROR) 310 end 311 312 -- Buttons 313 local buttonY = WINDOW_HEIGHT - BUTTON_HEIGHT - PADDING 314 local cancelX = PADDING 315 local okX = WINDOW_WIDTH - BUTTON_WIDTH - PADDING 316 317 -- Cancel button 318 gfx:fillRect(cancelX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT, COLOR_BUTTON_CANCEL) 319 gfx:drawRect(cancelX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT, COLOR_BORDER) 320 gfx:drawText(cancelX + 25, buttonY + 10, "Cancel", COLOR_TEXT) 321 322 -- OK button 323 gfx:fillRect(okX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT, COLOR_BUTTON_OK) 324 gfx:drawRect(okX, buttonY, BUTTON_WIDTH, BUTTON_HEIGHT, COLOR_BORDER) 325 gfx:drawText(okX + 35, buttonY + 10, "OK", COLOR_TEXT) 326 end 327 328 -- Click handler 329 promptWindow.onClick = function(mx, my) 330 local buttonY = WINDOW_HEIGHT - BUTTON_HEIGHT - PADDING 331 local cancelX = PADDING 332 local okX = WINDOW_WIDTH - BUTTON_WIDTH - PADDING 333 334 -- Check Cancel button 335 if mx >= cancelX and mx < cancelX + BUTTON_WIDTH and 336 my >= buttonY and my < buttonY + BUTTON_HEIGHT then 337 cancelPrompt() 338 end 339 340 -- Check OK button 341 if mx >= okX and mx < okX + BUTTON_WIDTH and 342 my >= buttonY and my < buttonY + BUTTON_HEIGHT then 343 submitPassword() 344 end 345 end 346 347 -- Close handler 348 promptWindow.onClose = function() 349 cancelPrompt() 350 return false -- Allow close 351 end 352 353 -- Keyboard input handler 354 promptWindow.inputCallback = function(key, scancode) 355 -- Only process key down events (scancode < 128) 356 if scancode >= 128 then 357 return 358 end 359 360 local baseScancode = scancode % 128 361 362 -- Handle Enter key (scancode 28) 363 if baseScancode == 28 then 364 submitPassword() 365 return 366 end 367 368 -- Handle Escape key (scancode 1) 369 if baseScancode == 1 then 370 cancelPrompt() 371 return 372 end 373 374 -- Handle Backspace (scancode 14) 375 if baseScancode == 14 then 376 if #currentPassword > 0 then 377 currentPassword = currentPassword:sub(1, -2) 378 errorMessage = nil 379 if promptWindow then 380 promptWindow:markDirty() 381 end 382 end 383 return 384 end 385 386 -- Handle printable characters 387 if key and type(key) == "string" and #key == 1 then 388 -- Filter out control characters 389 local byte = string.byte(key) 390 if byte >= 32 and byte <= 126 then 391 currentPassword = currentPassword .. key 392 errorMessage = nil 393 if promptWindow then 394 promptWindow:markDirty() 395 end 396 end 397 end 398 end 399 400 -- Focus handler - set this window as active when shown 401 promptWindow.onFocus = function() 402 if sys and sys.setActiveWindow then 403 sys.setActiveWindow(promptWindow) 404 end 405 end 406 end 407 408 -- Initialize 409 createPromptWindow() 410 411 -- Cursor blink timer (simple implementation) 412 -- In a real app, this would be a proper timer 413 local function updateCursor() 414 local now = os.clock() 415 if now - lastBlinkTime > 0.5 then 416 cursorVisible = not cursorVisible 417 lastBlinkTime = now 418 if promptWindow and promptWindow.visible then 419 promptWindow:markDirty() 420 end 421 end 422 end 423 424 -- Export admin.requestPermission function 425 app:export({ 426 name = "admin.requestPermission", 427 func = function(pid, permission, callback) 428 osprint("admin.requestPermission called: PID=" .. tostring(pid) .. ", permission=" .. tostring(permission) .. "\n") 429 430 -- Validate inputs 431 if type(pid) ~= "number" then 432 osprint("ERROR: pid must be a number\n") 433 if callback then pcall(callback, false) end 434 return false 435 end 436 437 if type(permission) ~= "string" then 438 osprint("ERROR: permission must be a string\n") 439 if callback then pcall(callback, false) end 440 return false 441 end 442 443 -- Set up request 444 currentRequest.type = "permission" 445 currentRequest.pid = pid 446 currentRequest.value = permission 447 currentCallback = callback 448 currentPassword = "" 449 errorMessage = nil 450 451 -- Show prompt window and set as active 452 if promptWindow then 453 promptWindow:show() 454 promptWindow:markDirty() 455 if sys and sys.setActiveWindow then 456 sys.setActiveWindow(promptWindow) 457 end 458 end 459 460 return true 461 end, 462 args = "pid=number,permission=string,callback=function", 463 rets = "boolean", 464 description = "Request administrator permission to grant a permission to a process" 465 }) 466 467 -- Export admin.requestPath function 468 app:export({ 469 name = "admin.requestPath", 470 func = function(pid, path, callback) 471 osprint("admin.requestPath called: PID=" .. tostring(pid) .. ", path=" .. tostring(path) .. "\n") 472 473 -- Validate inputs 474 if type(pid) ~= "number" then 475 osprint("ERROR: pid must be a number\n") 476 if callback then pcall(callback, false) end 477 return false 478 end 479 480 if type(path) ~= "string" then 481 osprint("ERROR: path must be a string\n") 482 if callback then pcall(callback, false) end 483 return false 484 end 485 486 -- Set up request 487 currentRequest.type = "path" 488 currentRequest.pid = pid 489 currentRequest.value = path 490 currentCallback = callback 491 currentPassword = "" 492 errorMessage = nil 493 494 -- Show prompt window and set as active 495 if promptWindow then 496 promptWindow:show() 497 promptWindow:markDirty() 498 if sys and sys.setActiveWindow then 499 sys.setActiveWindow(promptWindow) 500 end 501 end 502 503 return true 504 end, 505 args = "pid=number,path=string,callback=function", 506 rets = "boolean", 507 description = "Request administrator permission to grant a path to a process" 508 }) 509 510 -- Main loop - update cursor blink 511 -- Note: This is a simple polling approach. A proper implementation would use timers 512 if osprint then 513 osprint("Password prompt app initialized. Exported: admin.requestPermission, admin.requestPath\n") 514 end 515 516 -- Keep updating cursor 517 local function mainLoop() 518 while true do 519 updateCursor() 520 -- Yield to prevent blocking (if your OS supports coroutines) 521 -- coroutine.yield() 522 end 523 end 524 525 -- Note: Depending on your OS architecture, you might need to integrate 526 -- the cursor update into the window's event loop differently