diff --git a/.github/assets/preview-password.png b/.github/assets/preview-password.png new file mode 100644 index 0000000..b08a68e Binary files /dev/null and b/.github/assets/preview-password.png differ diff --git a/.github/assets/preview-pin.png b/.github/assets/preview-pin.png new file mode 100644 index 0000000..466191d Binary files /dev/null and b/.github/assets/preview-pin.png differ diff --git a/.luarc.json b/.luarc.json new file mode 100644 index 0000000..23c9713 --- /dev/null +++ b/.luarc.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://raw.githubusercontent.com/LuaLS/vscode-lua/master/setting/schema.json", + "workspace.library": [ + "~/koreader-base/ffi", + "~/koreader/frontend" + ], + "runtime.version": "LuaJIT", + "hint.enable": false +} \ No newline at end of file diff --git a/README.md b/README.md index 0e92ba3..e95ff4d 100644 --- a/README.md +++ b/README.md @@ -1,16 +1,61 @@ -# screenlock_koreader_plugin -This is a small plugin to enable locking the screen of the KOReader. +# KOReader Plugin: ScreenLock -## Setup -1. Put `screenlock.koplugin` into the `kodreader/plugins` directory. -2. Change the hardcoded password at the top of the main.lua from 1234 to something else. +This plugin adds a simple pin-based lock screen to KOReader. -The plugin will automatically activate on resume from suspend. There is also a screenlock menu entry added. +> [!CAUTION] +> This plugin provides **basic protection only**. +> It is **not a security solution** and will not stop a determined or technically skilled attacker. +> Do not rely on it to protect sensitive data. -### Hide Content Feature -Change `hide_content` bool to `true` to enlarge the input box and hide the screen until the password is entered correctly. This stops unauthorized users from seeing what books you're reading even if they turn on the device. Set bool to `false` to return to the small password box. +## Features -> [!CAUTION] -> This plugin is made for basic protection, not security — it may not protect your device from an experienced attacker. -> -> Always keep your device out of the hands of real threats. \ No newline at end of file +- Lock KOReader with a password +- Automatically locks after device suspend / resume +- Optional blank (white) screen to hide document contents +- On-screen password keyboard +- Menu actions for locking and password management + +## Installation + +1. Copy the `screenlock.koplugin` folder into: + ``` + koreader/plugins/ + ``` +2. Start (or restart) KOReader. +3. Open the KOReader menu and select **ScreenLock**. + +## Usage + +### Lock the screen + +- Open the KOReader menu +- Go to **ScreenLock** +- Select **Lock now** + +> or + +- Press power/lock button + +### Change the password + +- Open **ScreenLock** in the menu +- Select **Change password** +- Enter your current password, then set a new one + +> **Default password:** `1234` + +## Behavior Notes + +- The lock screen **automatically activates when the device resumes from sleep** +- Pressing **Cancel** on the password prompt puts the device back to sleep +- If enabled, screen contents are hidden while locked + +## Preview + + + +## Limitations + +- This plugin does **not encrypt files or memory** +- It only protects access within KOReader +- Removing the plugin or editing settings files can bypass the lock diff --git a/screenlock.koplugin/_meta.lua b/screenlock.koplugin/_meta.lua index 619aea4..3866bb7 100644 --- a/screenlock.koplugin/_meta.lua +++ b/screenlock.koplugin/_meta.lua @@ -3,7 +3,6 @@ local _ = require("gettext") return { name = "screenlock", fullname = _("ScreenLock"), - description = _([[This plugin lets you lock your screen with a password, + description = _([[This plugin lets you lock your screen with a password, either triggered from the menu or automatically upon device wake-up.]]), } - diff --git a/screenlock.koplugin/main.lua b/screenlock.koplugin/main.lua index 0be930c..f0cd29e 100644 --- a/screenlock.koplugin/main.lua +++ b/screenlock.koplugin/main.lua @@ -1,138 +1,297 @@ +local Blitbuffer = require("ffi/blitbuffer") +local sha2 = require("ffi/sha2") + +local _ = require("gettext") +local Device = require("device") local Dispatcher = require("dispatcher") +local DataStorage = require("datastorage") +local LuaSettings = require("luasettings") + local UIManager = require("ui/uimanager") local WidgetContainer = require("ui/widget/container/widgetcontainer") - local InputDialog = require("ui/widget/inputdialog") local InfoMessage = require("ui/widget/infomessage") -local _ = require("gettext") +local VirtualKeyboard = require("ui/widget/virtualkeyboard") -local ScreenLock = WidgetContainer:extend{ +local Screen = Device.screen +local DefaultPassword = sha2.sha256("1234") + +--[[ Fullscreen Overlay Widget ]] + +local FullscreenOverlay = WidgetContainer:extend {} + +function FullscreenOverlay:init() + self.covers_fullscreen = true + self.dimen = Screen:getSize() +end + +function FullscreenOverlay:paintTo(bb, x, y) + bb:fill(Blitbuffer.COLOR_WHITE) +end + +--[[ Screen Lock Widget ]] + +local ScreenLock = WidgetContainer:extend { name = "screenlock_inputdialog_buttons", is_doc_only = false, - locked = false, -- Track locked state - password = "1234", -- Your hard-coded password - hide_content = true, -- Hide screen content before password is entered + locked = false, + background_widget = nil, + settings_file = DataStorage:getSettingsDir() .. "/screen_lock.lua", + settings = nil, + + password_hash = DefaultPassword, + hide_content = true } ------------------------------------------------------------------------------- --- REGISTER DISPATCHER ACTIONS ------------------------------------------------------------------------------- +function ScreenLock:loadSettings() + if self.settings then + return + end + + self.settings = LuaSettings:open(self.settings_file) + + self.password_hash = self.settings:readSetting("password_hash") or DefaultPassword + self.hide_content = self.settings:readSetting("hide_content") == true +end + +function ScreenLock:onFlushSettings() + if self.settings then + self.settings:saveSetting("password_hash", self.password_hash) + self.settings:saveSetting("hide_content", self.hide_content) + + self.settings:flush() + end +end + +-- Register dispatcher actions function ScreenLock:onDispatcherRegisterActions() Dispatcher:registerAction("screenlock_inputdialog_buttons_lock_screen", { category = "none", event = "LockScreenButtons", title = _("Lock Screen (InputDialog + Buttons)"), - filemanager = true, + filemanager = true }) end ------------------------------------------------------------------------------- --- INIT (including wake-up handling via onResume) ------------------------------------------------------------------------------- +-- Initialize widget and register wakeup-handler function ScreenLock:init() - -- 1) Register dispatcher action + self:loadSettings() + + -- Initialize fullscreen widget + self.background_widget = FullscreenOverlay:new() + + -- Register dispatcher action self:onDispatcherRegisterActions() - - -- 2) Add to main menu + + -- Add to main menu self.ui.menu:registerToMainMenu(self) - -- 3) Override onResume to handle device wake-up - function self:onResume() + -- Safe onResume override to handle device wake-up + local originalResume = self.onResume + self.onResume = function(self) + if originalResume then originalResume(self) end + if not self.locked then self:lockScreen() end end end ------------------------------------------------------------------------------- --- LOCK SCREEN ------------------------------------------------------------------------------- +-- Lock the screen now function ScreenLock:lockScreen() self.locked = true + + -- Show fullscreen overlay + if self.hide_content then + UIManager:show(self.background_widget) + end + + -- Show passowrd prompt self:showPasswordPrompt() end ------------------------------------------------------------------------------- --- SHOW PASSWORD PROMPT (USING BUTTONS ARRAY) --- "Cancel" button reopens the prompt, preventing escape ------------------------------------------------------------------------------- +-- Shows the password prompt and prevents escaping function ScreenLock:showPasswordPrompt() local dialog - dialog = InputDialog:new{ - title = _("Enter Password"), - input = "", - maskinput = true, + self:addKeyboard() + dialog = InputDialog:new { + title = _("Enter Password"), + input = "", text_type = "password", - hint = _("Password"), - fullscreen = self.hide_content, -- request full screen mode - use_available_height = self.hide_content, -- use available screen height even when keyboard is shown - buttons = { + buttons = { { + { + text = _("Cancel"), + callback = function() + -- Sleep the device + UIManager:suspend() + end + }, { - { - text = _("Cancel"), - callback = function() - UIManager:show( - InfoMessage:new{ - text = _("You must enter the correct password!"), - timeout = 1 - } - ) + text = _("OK"), + is_enter_default = true, + callback = function() + local password_input = dialog:getInputText() + local password_hash = sha2.sha256(password_input) + + if password_hash == self.password_hash then + self.locked = false + UIManager:close(dialog) + UIManager:close(self.background_widget, "full") + self:restoreKeyboard() + else + UIManager:show(InfoMessage:new { + text = _("Wrong password! Try again."), + timeout = 1 + }) + + UIManager:close(dialog) + self:restoreKeyboard() self:showPasswordPrompt() end - }, - { - text = _("OK"), - is_enter_default = true, - callback = function() - local userInput = dialog:getInputText() - if userInput == self.password then - self.locked = false - UIManager:close(dialog) - UIManager:show( - InfoMessage:new{ - text = _("Screen unlocked."), - timeout = 1 - } - ) - else - UIManager:show( - InfoMessage:new{ - text = _("Wrong password! Try again."), - timeout = 1 - } - ) - UIManager:close(dialog) - self:showPasswordPrompt() - end - end - }, + end } - }, + } } } + UIManager:show(dialog) - dialog:onShowKeyboard() -- Immediately open the on-screen keyboard + dialog:onShowKeyboard() -- Immediately open the on-screen keyboard end ------------------------------------------------------------------------------- --- DISPATCHER HANDLER ------------------------------------------------------------------------------- +-- Dispatch handler when screen is locked function ScreenLock:onLockScreenButtons() self:lockScreen() return true end ------------------------------------------------------------------------------- --- MAIN MENU ENTRY ------------------------------------------------------------------------------- +-- Shows a dialog to change the password +function ScreenLock:changePassword() + self:addKeyboard() + -- Ask for old password + local old_dialog + old_dialog = InputDialog:new { + title = _("Enter old password"), + input = "", + text_type = "password", + buttons = { { + { + text = _("Cancel"), + callback = function() + UIManager:close(old_dialog) + self:restoreKeyboard() + end + }, + { + text = _("OK"), + is_enter_default = true, + callback = function() + local old_password_input = old_dialog:getInputText() + local old_password_hash = sha2.sha256(old_password_input) + + if old_password_hash == self.password_hash then + UIManager:close(old_dialog) + + -- Ask for new password + local new_dialog + new_dialog = InputDialog:new { + title = _("Enter new password"), + input = "", + text_type = "password", + buttons = { { + { + text = _("Cancel"), + callback = function() + UIManager:close(new_dialog) + self:restoreKeyboard() + end + }, + { + text = _("OK"), + is_enter_default = true, + callback = function() + local new_password_input = new_dialog:getInputText() + local new_password_hash = sha2.sha256(new_password_input) + + self.password_hash = new_password_hash + + UIManager:show(InfoMessage:new { + text = _("Password changed!"), + timeout = 1 + }) + + UIManager:close(new_dialog) + self:restoreKeyboard() + end + } + } } + } + + UIManager:show(new_dialog) + new_dialog:onShowKeyboard() + else + UIManager:show(InfoMessage:new { + text = _("Wrong password! Try again."), + timeout = 1 + }) + + UIManager:close(old_dialog) + self:restoreKeyboard() + self:changePassword() + end + end + } + } } + } + + UIManager:show(old_dialog) + old_dialog:onShowKeyboard() +end + +-- Register main menu entry function ScreenLock:addToMainMenu(menu_items) - menu_items.screenlock_inputdialog_buttons = { - text = _("Lock Screen"), - callback = function() - self:lockScreen() - end - } + menu_items.screenlock_inputdialog_buttons = { + text = _("Screenlock"), + sub_item_table = { + { + text = _("Lock now"), + callback = function() + -- Sleep the device + UIManager:suspend() + end, + separator = true, + }, + { + text = _("Change password"), + callback = function() + self:changePassword() + end, + }, + { + text = _("Hide screen content"), + checked_func = function() + return self.hide_content + end, + callback = function() + self.hide_content = not self.hide_content + end, + }, + } + } +end + +function ScreenLock:addKeyboard() + VirtualKeyboard.lang_to_keyboard_layout[_ "ScreenLockPassword"] = "password_keyboard" + VirtualKeyboard.layout_file = "password_keyboard" + self.original_keyboard_layout = G_reader_settings:readSetting("keyboard_layout") + G_reader_settings:saveSetting("keyboard_layout", "ScreenLockPassword") +end + +function ScreenLock:restoreKeyboard() + VirtualKeyboard.lang_to_keyboard_layout[_ "ScreenLockPassword"] = nil + VirtualKeyboard.layout_file = nil + + G_reader_settings:saveSetting("keyboard_layout", self.original_keyboard_layout) end return ScreenLock diff --git a/screenlock.koplugin/ui/data/keyboardlayouts/password_keyboard.lua b/screenlock.koplugin/ui/data/keyboardlayouts/password_keyboard.lua new file mode 100644 index 0000000..8feeb7b --- /dev/null +++ b/screenlock.koplugin/ui/data/keyboardlayouts/password_keyboard.lua @@ -0,0 +1,38 @@ +--[[ + Keyboard layout for number password +]] + +return { + min_layer = 1, + max_layer = 1, + shiftmode_keys = {}, + symbolmode_keys = {}, + utf8mode_keys = {}, + keys = { + { + { "1" }, + { "2" }, + { "3" }, + }, + { + { "4" }, + { "5" }, + { "6" }, + }, + { + { "7" }, + { "8" }, + { "9" }, + }, + { + { label = "", width = 1.0, bold = false }, --delete + { "0" }, + { + label = "⮠", + "\n", + width = 1.0, + bold = true + }, + }, + }, +}