cshwayze

Keypad Door Auth Server (ComputerCraft)

Oct 10th, 2025 (edited)
100
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 16.56 KB | Gaming | 0 0
  1. --============================================================--
  2. -- auth_server.lua (FINAL VERSION)
  3. -- Features:
  4. -- ✔ Admin PIN (hashed)
  5. -- ✔ Local admin console
  6. -- ✔ Remote admin (secure login)
  7. -- ✔ Lockdown mode
  8. -- ✔ Door fob support (door_list + verify)
  9. -- ✔ Audit logs (persistent)
  10. -- ✔ Heartbeat to controllers
  11. -- ✔ Autosave + DB persistence
  12. -- ✔ Controller auto-registration
  13. --============================================================--
  14.  
  15. ------------------ Config ------------------
  16. local PROTOCOL = "doorAuth.v1"
  17. local OPEN_EVENT = "doorAuth.open.v1"
  18. local HEARTBEAT_EVENT = "doorAuth.heartbeat.v1"
  19. local HOST_NAME = "DoorAuthServer"
  20.  
  21. local DB_PATH = "door_db.json"
  22. local ADMIN_PATH = "admin.json"
  23. local LOG_PATH = "door_logs.json"
  24.  
  25. local SAVE_INTERVAL = 30
  26. local ADMIN_TIMEOUT = 120 -- 2 minutes inactivity
  27. local HEARTBEAT_RATE = 10
  28. local LOG_MAX = 1000
  29. --------------------------------------------
  30.  
  31. ------------------ Utils -------------------
  32. local function readAll(path)
  33. if not fs.exists(path) then return nil end
  34. local h = fs.open(path,"r")
  35. local d = h.readAll()
  36. h.close()
  37. return d
  38. end
  39.  
  40. local function writeAll(path,data)
  41. local h = fs.open(path,"w")
  42. h.write(data)
  43. h.close()
  44. end
  45.  
  46. local function jsonEncode(tbl)
  47. if textutils.serializeJSON then
  48. return textutils.serializeJSON(tbl)
  49. else
  50. return textutils.serialize(tbl)
  51. end
  52. end
  53.  
  54. local function jsonDecode(s)
  55. if not s then return nil end
  56. if textutils.unserializeJSON then
  57. return textutils.unserializeJSON(s)
  58. else
  59. return textutils.unserialize(s)
  60. end
  61. end
  62.  
  63. local function trim(s)
  64. s = tostring(s or "")
  65. return s:gsub("^%s+",""):gsub("%s+$","")
  66. end
  67.  
  68. local function clockString()
  69. local ok,t = pcall(os.time)
  70. if ok and textutils.formatTime then
  71. return textutils.formatTime(t, true)
  72. end
  73. return "??:??"
  74. end
  75. --------------------------------------------
  76.  
  77. ------------------ State -------------------
  78. local db = { doors = {} }
  79. local controllersByTag = {}
  80. local lockdown = false
  81. local logs = {}
  82.  
  83. local admin = {
  84. pinHash = nil,
  85. loggedIn = false,
  86. remoteToken = nil,
  87. lastAction = 0
  88. }
  89. --------------------------------------------
  90.  
  91. ------------------ Hashing -----------------
  92. -- Fallback SHA256-like hash
  93. local function sha256(str)
  94. local h=0
  95. for i=1,#str do
  96. h=(h*31 + str:byte(i)) % 2^31
  97. end
  98. return tostring(h)
  99. end
  100. --------------------------------------------
  101.  
  102. ------------------ Admin Auth --------------
  103. local function loadAdmin()
  104. if not fs.exists(ADMIN_PATH) then
  105. print("=== FIRST-TIME ADMIN SETUP ===")
  106. while true do
  107. write("New Admin PIN: ") local p1=read("*")
  108. write("Confirm PIN: ") local p2=read("*")
  109. if p1==p2 and #p1>=4 then
  110. admin.pinHash = sha256(p1)
  111. writeAll(ADMIN_PATH, admin.pinHash)
  112. print("Admin PIN saved.")
  113. break
  114. end
  115. print("Pins did not match or too short. Try again.")
  116. end
  117. else
  118. admin.pinHash = trim(readAll(ADMIN_PATH))
  119. end
  120. end
  121.  
  122. local function isAdminValid()
  123. if not admin.loggedIn then return false end
  124. if (os.epoch("utc") - admin.lastAction) > ADMIN_TIMEOUT*1000 then
  125. admin.loggedIn = false
  126. admin.remoteToken = nil
  127. print("[ADMIN] Session expired.")
  128. return false
  129. end
  130. return true
  131. end
  132.  
  133. local function requireAdmin()
  134. if isAdminValid() then
  135. admin.lastAction = os.epoch("utc")
  136. return true
  137. end
  138.  
  139. write("Admin PIN: ")
  140. local attempt = read("*")
  141. if sha256(attempt) == admin.pinHash then
  142. admin.loggedIn = true
  143. admin.lastAction = os.epoch("utc")
  144. print("[ADMIN] Login OK")
  145. return true
  146. end
  147.  
  148. print("[ADMIN] Incorrect PIN.")
  149. return false
  150. end
  151. --------------------------------------------
  152.  
  153. ------------------ Logs ---------------------
  154. local function loadLogs()
  155. local raw = readAll(LOG_PATH)
  156. if not raw then logs = {} return end
  157. local parsed = jsonDecode(raw)
  158. logs = type(parsed)=="table" and parsed or {}
  159. end
  160.  
  161. local function saveLogs()
  162. writeAll(LOG_PATH, jsonEncode(logs))
  163. end
  164.  
  165. local function logEvent(evt)
  166. local entry = {
  167. time = clockString(),
  168. ts = os.epoch("utc"),
  169. event = evt.event or "unknown",
  170. tag = evt.tag,
  171. ok = evt.ok,
  172. source = evt.source,
  173. detail = evt.detail
  174. }
  175. table.insert(logs, entry)
  176. if #logs > LOG_MAX then table.remove(logs,1) end
  177. end
  178. --------------------------------------------
  179.  
  180. ------------- Persistence ------------------
  181. local function loadDB()
  182. local raw = readAll(DB_PATH)
  183. if raw then
  184. local parsed = jsonDecode(raw)
  185. if parsed and parsed.doors then
  186. db = parsed
  187. print("[DB] Loaded.")
  188. return
  189. end
  190. end
  191. print("[DB] Starting fresh.")
  192. end
  193.  
  194. local function saveDB()
  195. writeAll(DB_PATH, jsonEncode(db))
  196. print("[DB] Saved.")
  197. end
  198. --------------------------------------------
  199.  
  200. --------------- Networking -----------------
  201. local function openModems()
  202. for _,side in ipairs(rs.getSides()) do
  203. if peripheral.getType(side)=="modem" then
  204. if not rednet.isOpen(side) then rednet.open(side) end
  205. end
  206. end
  207. end
  208.  
  209. local function broadcastOpen(tag, duration)
  210. local set = controllersByTag[tag]
  211. if not set then return end
  212. for id,_ in pairs(set) do
  213. rednet.send(id, {type="open", tag=tag, duration=duration}, OPEN_EVENT)
  214. end
  215. end
  216. --------------------------------------------
  217.  
  218. ------------- Door Helpers -----------------
  219. local function ensureDoor(tag)
  220. db.doors[tag] = db.doors[tag] or { pins={}, openTime=3 }
  221. return db.doors[tag]
  222. end
  223.  
  224. local function hasPin(tag, pin)
  225. local d = db.doors[tag]
  226. if not d then return false end
  227. pin = tostring(pin)
  228. for _,p in ipairs(d.pins) do
  229. if tostring(p) == pin then return true end
  230. end
  231. return false
  232. end
  233.  
  234. local function addPin(tag, pin)
  235. ensureDoor(tag)
  236. pin = tostring(pin)
  237. for _,p in ipairs(db.doors[tag].pins) do
  238. if p == pin then return false end
  239. end
  240. table.insert(db.doors[tag].pins, pin)
  241. return true
  242. end
  243.  
  244. local function removePin(tag,pin)
  245. local d=db.doors[tag]
  246. if not d then return false end
  247. local out,removed={},false
  248. pin=tostring(pin)
  249. for _,p in ipairs(d.pins) do
  250. if p~=pin then table.insert(out,p)
  251. else removed=true end
  252. end
  253. d.pins=out
  254. return removed
  255. end
  256. --------------------------------------------
  257.  
  258. ---------------- Remote Admin --------------
  259. local function handleRemoteAdmin(sender,msg)
  260.  
  261. --------------------------------------------------
  262. -- REMOTE LOGIN
  263. --------------------------------------------------
  264. if msg.type=="admin_login" then
  265. local stamp = tostring(msg.timestamp or "")
  266. local sig = tostring(msg.sig or "")
  267.  
  268. local expected = sha256(admin.pinHash .. stamp)
  269.  
  270. if sig == expected then
  271. local token = tostring(math.random(100000,999999))..tostring(os.epoch("utc"))
  272. admin.loggedIn = true
  273. admin.remoteToken= token
  274. admin.lastAction = os.epoch("utc")
  275.  
  276. logEvent({event="admin_login", ok=true, source="remote#"..sender})
  277. rednet.send(sender,{type="admin_login_ok", token=token},PROTOCOL)
  278. else
  279. logEvent({event="admin_login", ok=false, source="remote#"..sender, detail="bad_sig"})
  280. rednet.send(sender,{type="admin_login_fail"},PROTOCOL)
  281. end
  282.  
  283. return true
  284. end
  285.  
  286. --------------------------------------------------
  287. -- REMOTE COMMANDS
  288. --------------------------------------------------
  289. if msg.type=="admin_cmd" then
  290. if msg.token ~= admin.remoteToken or not isAdminValid() then
  291. rednet.send(sender,{type="admin_denied"},PROTOCOL)
  292. return true
  293. end
  294.  
  295. admin.lastAction = os.epoch("utc")
  296.  
  297. if msg.cmd=="list" then
  298. rednet.send(sender,{type="admin_list", doors=db.doors},PROTOCOL)
  299.  
  300. elseif msg.cmd=="show" then
  301. rednet.send(sender,{type="admin_show", tag=msg.tag, door=db.doors[msg.tag]},PROTOCOL)
  302.  
  303. elseif msg.cmd=="add" then
  304. local ok=addPin(msg.tag,msg.pin)
  305. logEvent({event="pin_add", tag=msg.tag, ok=ok, source="remote"})
  306. rednet.send(sender,{type="admin_result",ok=ok},PROTOCOL)
  307.  
  308. elseif msg.cmd=="del" then
  309. local ok=removePin(msg.tag,msg.pin)
  310. logEvent({event="pin_del", tag=msg.tag, ok=ok, source="remote"})
  311. rednet.send(sender,{type="admin_result",ok=ok},PROTOCOL)
  312.  
  313. elseif msg.cmd=="remove" then
  314. db.doors[msg.tag]=nil
  315. logEvent({event="door_remove", tag=msg.tag, ok=true, source="remote"})
  316. rednet.send(sender,{type="admin_result",ok=true},PROTOCOL)
  317.  
  318. elseif msg.cmd=="opentime" then
  319. ensureDoor(msg.tag)
  320. db.doors[msg.tag].openTime = tonumber(msg.seconds)
  321. logEvent({
  322. event="opentime_set", tag=msg.tag,
  323. ok=true, source="remote",
  324. detail="seconds="..tostring(msg.seconds)
  325. })
  326. rednet.send(sender,{type="admin_result",ok=true},PROTOCOL)
  327.  
  328. elseif msg.cmd=="lockdown_on" then
  329. lockdown=true
  330. logEvent({event="lockdown_on", ok=true, source="remote"})
  331. rednet.send(sender,{type="admin_result",ok=true,state="locked"},PROTOCOL)
  332.  
  333. elseif msg.cmd=="lockdown_off" then
  334. lockdown=false
  335. logEvent({event="lockdown_off", ok=true, source="remote"})
  336. rednet.send(sender,{type="admin_result",ok=true,state="unlocked"},PROTOCOL)
  337.  
  338. elseif msg.cmd=="open" then
  339. if lockdown then
  340. logEvent({event="remote_open", tag=msg.tag, ok=false, source="remote", detail="blocked_by_lockdown"})
  341. rednet.send(sender,{type="admin_result",ok=false,reason="lockdown"},PROTOCOL)
  342. return true
  343. end
  344. local d=db.doors[msg.tag]
  345. local dur=(d and d.openTime) or 3
  346. broadcastOpen(msg.tag,dur)
  347. logEvent({event="remote_open",tag=msg.tag,ok=true,source="remote"})
  348. rednet.send(sender,{type="admin_result",ok=true},PROTOCOL)
  349.  
  350. elseif msg.cmd=="logs" then
  351. rednet.send(sender,{type="admin_logs", logs=logs},PROTOCOL)
  352. end
  353.  
  354. return true
  355. end
  356.  
  357. return false
  358. end
  359. --------------------------------------------
  360.  
  361. ---------------- Handlers ------------------
  362. local function handleMessage(sender,msg,proto)
  363. if proto~=PROTOCOL or type(msg)~="table" then return end
  364.  
  365. -- remote admin first
  366. if handleRemoteAdmin(sender,msg) then return end
  367.  
  368. -------- door_list (for door_fob) --------
  369. if msg.type=="door_list" then
  370. local tags={}
  371. for tag,_ in pairs(db.doors) do table.insert(tags,tag) end
  372. rednet.send(sender,{type="door_list",tags=tags},PROTOCOL)
  373. return
  374. end
  375.  
  376. -------------- verify (keypads + fob) -----
  377. if msg.type=="verify" then
  378. local tag=trim(msg.tag)
  379. local pin=trim(msg.pin)
  380.  
  381. if lockdown then
  382. rednet.send(sender,{type="verify_result",ok=false,tag=tag},PROTOCOL)
  383. logEvent({event="pin_attempt",tag=tag,ok=false,source="keypad#"..sender,detail="lockdown"})
  384. return
  385. end
  386.  
  387. local ok = hasPin(tag,pin)
  388. rednet.send(sender,{type="verify_result",ok=ok,tag=tag},PROTOCOL)
  389.  
  390. logEvent({
  391. event="pin_attempt",
  392. tag=tag, ok=ok,
  393. source="keypad#"..sender
  394. })
  395.  
  396. if ok then
  397. local d=db.doors[tag]
  398. local dur=(d and d.openTime) or 3
  399. broadcastOpen(tag,dur)
  400. logEvent({event="door_open",tag=tag,ok=true,source="keypad"})
  401. end
  402.  
  403. return
  404. end
  405.  
  406. -------------- controller registration ----
  407. if msg.type=="registerController" then
  408. local tag=trim(msg.tag)
  409. controllersByTag[tag]=controllersByTag[tag] or {}
  410. controllersByTag[tag][sender]=true
  411. rednet.send(sender,{type="register_ack",tag=tag},PROTOCOL)
  412. logEvent({event="controller_register",tag=tag,ok=true,source="ctrl#"..sender})
  413. return
  414. end
  415.  
  416. if msg.type=="unregisterController" then
  417. local tag=trim(msg.tag)
  418. if controllersByTag[tag] then controllersByTag[tag][sender]=nil end
  419. rednet.send(sender,{type="unregister_ack"},PROTOCOL)
  420. logEvent({event="controller_unregister",tag=tag,ok=true,source="ctrl#"..sender})
  421. return
  422. end
  423. end
  424. --------------------------------------------
  425.  
  426. ---------------- Console -------------------
  427. local function help()
  428. print([[Commands:
  429. help
  430. controllers
  431. save
  432. reboot
  433. cls
  434.  
  435. -- Admin commands --
  436. add <tag> <pin>
  437. del <tag> <pin>
  438. opentime <tag> <seconds>
  439. remove <tag>
  440. show <tag>
  441. list
  442. lockdown_on
  443. lockdown_off
  444. logs
  445. ]])
  446. end
  447.  
  448. local function printLogsConsole(maxLines)
  449. if not requireAdmin() then return end
  450. maxLines=maxLines or 40
  451. local n=#logs
  452. local start = math.max(1, n-maxLines+1)
  453. for i=start,n do
  454. local e=logs[i]
  455. local status = e.ok==nil and "" or (e.ok and "OK" or "FAIL")
  456. print(("[%4d] %s %-14s tag=%s %s %s")
  457. :format(
  458. i,
  459. e.time or "??:??",
  460. e.event or "?",
  461. e.tag or "-",
  462. status,
  463. e.detail or ""
  464. ))
  465. end
  466. end
  467.  
  468. local function consoleLoop()
  469. help()
  470. while true do
  471. term.setTextColor(colors.yellow) write("> ")
  472. term.setTextColor(colors.white)
  473. local line=read()
  474.  
  475. local args={}
  476. for w in line:gmatch("%S+") do table.insert(args,w) end
  477. local cmd=args[1]
  478.  
  479. if cmd=="help" then help()
  480. elseif cmd=="cls" or cmd=="clear" then term.clear() term.setCursorPos(1,1)
  481.  
  482. elseif cmd=="controllers" then
  483. for tag,set in pairs(controllersByTag) do
  484. local ids={}
  485. for id,_ in pairs(set) do table.insert(ids,id) end
  486. print(tag.." -> "..table.concat(ids,",")) end
  487.  
  488. elseif cmd=="save" then saveDB() saveLogs()
  489. elseif cmd=="reboot" then saveDB() saveLogs() sleep(0.2) os.reboot()
  490.  
  491. elseif cmd=="list" then
  492. if requireAdmin() then
  493. for tag,d in pairs(db.doors) do
  494. print(("- %s (pins:%d, open:%ds)")
  495. :format(tag,#d.pins,d.openTime or 3))
  496. end
  497. end
  498.  
  499. elseif cmd=="show" and args[2] then
  500. if requireAdmin() then
  501. local d=db.doors[args[2]]
  502. if not d then print("No such door.") else
  503. print("OpenTime:",d.openTime) print("Pins:")
  504. for _,p in ipairs(d.pins) do print(" "..p) end
  505. end
  506. end
  507.  
  508. elseif cmd=="add" and args[2] and args[3] then
  509. if requireAdmin() then
  510. local ok=addPin(args[2],args[3])
  511. logEvent({event="pin_add",tag=args[2],ok=ok,source="console"})
  512. print(ok and "Added." or "Already present.")
  513. end
  514.  
  515. elseif cmd=="del" and args[2] and args[3] then
  516. if requireAdmin() then
  517. local ok=removePin(args[2],args[3])
  518. logEvent({event="pin_del",tag=args[2],ok=ok,source="console"})
  519. print(ok and "Removed." or "Not found.")
  520. end
  521.  
  522. elseif cmd=="opentime" and args[2] and tonumber(args[3]) then
  523. if requireAdmin() then
  524. ensureDoor(args[2])
  525. db.doors[args[2]].openTime = tonumber(args[3])
  526. logEvent({event="opentime_set",tag=args[2],ok=true,source="console"})
  527. print("Updated.")
  528. end
  529.  
  530. elseif cmd=="remove" and args[2] then
  531. if requireAdmin() then
  532. db.doors[args[2]]=nil
  533. logEvent({event="door_remove",tag=args[2],ok=true,source="console"})
  534. print("Door removed.")
  535. end
  536.  
  537. elseif cmd=="lockdown_on" then
  538. if requireAdmin() then
  539. lockdown=true
  540. logEvent({event="lockdown_on",ok=true,source="console"})
  541. print("LOCKDOWN ENABLED")
  542. end
  543.  
  544. elseif cmd=="lockdown_off" then
  545. if requireAdmin() then
  546. lockdown=false
  547. logEvent({event="lockdown_off",ok=true,source="console"})
  548. print("Lockdown disabled.")
  549. end
  550.  
  551. elseif cmd=="logs" then
  552. printLogsConsole(40)
  553.  
  554. elseif cmd and cmd~="" then
  555. print("Unknown command.")
  556. end
  557. end
  558. end
  559. --------------------------------------------
  560.  
  561. ---------------- Network -------------------
  562. local function netLoop()
  563. while true do
  564. local id,msg,proto = rednet.receive()
  565. handleMessage(id,msg,proto)
  566. end
  567. end
  568.  
  569. local function autosaveLoop()
  570. while true do
  571. sleep(SAVE_INTERVAL)
  572. saveDB()
  573. saveLogs()
  574. end
  575. end
  576.  
  577. local function heartbeatLoop()
  578. while true do
  579. sleep(HEARTBEAT_RATE)
  580. for tag,set in pairs(controllersByTag) do
  581. for id,_ in pairs(set) do
  582. rednet.send(id,{type="hb"},HEARTBEAT_EVENT)
  583. end
  584. end
  585. end
  586. end
  587. --------------------------------------------
  588.  
  589. ------------------- Main -------------------
  590. term.setTextColor(colors.cyan)
  591. print("[DoorAuth Server] starting...")
  592. term.setTextColor(colors.white)
  593.  
  594. openModems()
  595. rednet.host(PROTOCOL, HOST_NAME)
  596. loadDB()
  597. loadLogs()
  598. loadAdmin()
  599. logEvent({event="server_start",ok=true,source="server"})
  600.  
  601. parallel.waitForAny(netLoop, consoleLoop, autosaveLoop, heartbeatLoop)
  602.  
Advertisement
Add Comment
Please, Sign In to add comment