Your school or employer manages your PC, and somewhere in that management someone turned off locking. Win+L does nothing. Ctrl+Alt+Del offers no Lock. The Start menu's account picture gives you Sign out, but not Lock. If you step away from a machine like that without doing anything, the next person to sit down is sitting down as you — your files, your browser sessions, your chat history.
Managed images disable locking for reasons that made sense to someone at some point: kiosk setups, shared lab machines, a policy template nobody revisited. This post is not about defeating your organization's policy — everything below uses documented Windows features or a plain window drawn on your own session, and if your IT department disabled locking on purpose, the right move is to ask them, not to work around them. This is for the far more common case: a machine where locking was collateral damage, and you just want to go to the toilet without leaving your session open.
The only test that matters
A lock is not a dark screen. A lock means one thing: to get back in, you must authenticate at the real Windows sign-in screen with the account password or PIN the machine already knows. Anything that fails that test hides your screen and nothing more. Keep that test in mind — it is what separates the methods below.
The normal ways, briefly
Win+L, Ctrl+Alt+Del → Lock, Start menu → your picture → Lock: all three end at the same Windows lock function, and on a managed image all three are typically removed together. If any of them works on your machine, you are done — use it and skip the rest of this post.
LockWorkStation: the direct call that sometimes survives
The lock those shortcuts invoke is an ordinary function in user32.dll, and you can call it yourself. Save this as Lock PC.bat and double-click it:
rundll32.exe user32.dll,LockWorkStation
Policies often remove the menu entries and the hotkey but forget the API itself, so this frequently still works when nothing else does. On genuinely hardened images it does nothing at all — the call is blocked the same way as the hotkey. It costs you ten seconds to find out which machine you have.
The screensaver trick: a lock that forgot to lock
The old workaround is a screensaver with "On resume, display logon screen" ticked. In theory the screen blanks and waking it demands your password. In practice, on the managed image we tested, the screen went dark and wiggling the mouse walked straight back in: that flag is read at logon, so changing it mid-session does nothing until you sign out and back in — at which point you no longer need the trick. It looks like a lock. It is not one.
tsdiscon: the real thing, hidden in plain sight
Windows ships a command for disconnecting a remote-desktop session, and it works on a local console session too. Save this as Lock PC - fallback.bat:
tsdiscon
Your session disconnects to the sign-in screen. Your apps keep running. Getting back in requires your real account password, checked by Windows itself — this passes the only test that matters, and it survived on the image where every menu entry was gone.
It has one quirk worth knowing before you rely on it: occasionally, after you sign back in, the mouse pointer is invisible. The pointer works; you just cannot see it. Press Ctrl+Shift+Esc to open Task Manager, then Esc to close it, and the cursor comes back. Annoying, harmless, and consistent.
The overlay option: a polite barrier you own
The last category is not a Windows lock at all — it is a window that covers the screen and refuses to go away without a password you choose. We built a small one for exactly this situation, the UniAuth Screen Locker, and it comes in two variants:
- Instant Lock. A single Python file with the password set in a line at the top. Double-click and the screen is locked. That is the entire interface.
- Locker with launcher. Opens a small setup window first: choose a session password and an optional public hint (shown on the lock screen), then press Lock. Unlocking returns to the setup window, so you can lock again with a different password each time.
Both show a clock and your name, block the Windows keys, Alt+Tab, Alt+F4 and friends via a low-level keyboard hook, and are ordinary readable Python with tkinter — no installer, no admin rights, and no network connection of any kind; the script never opens a socket.
One prerequisite first: Python. Both tools are ordinary .py files, so the machine needs Python 3 with tkinter — and tkinter is not an extra install, since it ships inside the standard Windows installer from python.org and in the Microsoft Store build. Neither route needs admin rights, which is exactly what makes this viable on a managed machine: the python.org installer has an install-just-for-me mode (leave "Install launcher for all users" unticked), and the Microsoft Store install is per-user by design. On a school PC — the environment this post is written for — there is a good chance Python is already installed, since it is the standard teaching language; open a terminal and try python --version. If it answers with a version number you are done; if not, the Store or the per-user installer takes a couple of minutes and never asks for the admin password everything else on the machine demands. After that, double-clicking the file is the entire deployment.
Be honest about what this is, though, because we have to be: Ctrl+Alt+Del → Sign out always escapes it. No application can trap that key combination — Windows reserves it for exactly this reason. So the overlay stops classmates, coworkers and passers-by. It does not stop someone willing to sign you out and end your session to get past it. That is a deterrence boundary, not a security boundary, and for a two-minute coffee run deterrence is usually the whole job.
Both scripts are published here in full: collapsed below to the lines that matter (in the instant one, the password is the line marked CHANGE THIS), each with the complete source one click away, and both as direct downloads — instant and with launcher. If you want it different — a longer password field, your own name on it — paste the source into any current AI assistant and describe the change. It is that kind of tool. And because you can read every line before running it, a lock screen you can inspect beats a lock screen you downloaded from a stranger — including from us.
# UniAuth Screen Locker - no admin needed
# INSTANT LOCK version: password is set below in the file, locks immediately on launch.
# Rebranded to uniauth.id/brand: monochrome neutral palette, Geist-style typography.
# Official UniAuth shield logo embedded as base64 PNG - no external files needed.
# Design: live clock + date, user avatar, caps-lock hint, dot-grid backdrop,
# lock-in / unlock / wrong-password animations.
# BEFORE FIRST USE: change the password below.
# If anything goes wrong, a file named locker_error.txt appears next to this file.
PASSWORD = "1234" # <<< CHANGE THIS to your own password
import ctypes
import getpass
import os
import random
import time
import traceback
from ctypes import wintypes
# ---------- Official UniAuth logo (from uniauth.id/brand) ----------
# Source SVG (oauth/mark-white.svg, white shield on transparent):
UNIAUTH_MARK_SVG = '''<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>'''
# Same mark, pre-rendered to a 96x96 transparent PNG, base64-encoded:
LOGO_PNG_B64 = (
"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"
)
# ---------- UniAuth brand tokens (dark theme, from uniauth.id/press) ----------
BG = "#0a0a0a" # --background
FG = "#fafafa" # --foreground / --primary
CARD = "#171717" # neutral-900
MUTED = "#262626" # --muted / --border
SUB = "#a3a3a3" # neutral-400
DIM = "#525252" # neutral-600
GRID = "#161616" # backdrop dots, barely above background
DESTR = "#7f1d1d" # --destructive (dark)
def _ease_out(t):
return 1 - (1 - t) ** 3
def main():
import tkinter as tk
import tkinter.font as tkfont
# ---------- low-level keyboard hook ----------
# Blocks: Win keys, Alt+Tab, Alt+F4, Alt+Esc, Alt+Space, Ctrl+Esc, Ctrl+Shift+Esc
# (Ctrl+Alt+Del cannot be blocked by any program - Windows reserves it.)
user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32
WH_KEYBOARD_LL = 13
WM_KEYDOWN = 0x0100
WM_SYSKEYDOWN = 0x0104
VK_TAB, VK_ESCAPE, VK_SPACE, VK_F4 = 0x09, 0x1B, 0x20, 0x73
VK_LWIN, VK_RWIN, VK_MENU, VK_CONTROL = 0x5B, 0x5C, 0x12, 0x11
VK_CAPITAL = 0x14
LRESULT = ctypes.c_longlong if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_long
class KBDLLHOOKSTRUCT(ctypes.Structure):
_fields_ = [("vkCode", wintypes.DWORD),
("scanCode", wintypes.DWORD),
("flags", wintypes.DWORD),
("time", wintypes.DWORD),
("dwExtraInfo", ctypes.c_size_t)]
HOOKPROC = ctypes.WINFUNCTYPE(LRESULT, ctypes.c_int, wintypes.WPARAM, wintypes.LPARAM)
user32.SetWindowsHookExW.restype = ctypes.c_void_p
user32.SetWindowsHookExW.argtypes = [ctypes.c_int, HOOKPROC, ctypes.c_void_p, wintypes.DWORD]
user32.CallNextHookEx.restype = LRESULT
user32.CallNextHookEx.argtypes = [ctypes.c_void_p, ctypes.c_int, wintypes.WPARAM, wintypes.LPARAM]
user32.UnhookWindowsHookEx.argtypes = [ctypes.c_void_p]
user32.GetAsyncKeyState.argtypes = [ctypes.c_int]
user32.GetKeyState.argtypes = [ctypes.c_int]
kernel32.GetModuleHandleW.restype = ctypes.c_void_p
def _pressed(vk):
return user32.GetAsyncKeyState(vk) & 0x8000
def hook_proc(nCode, wParam, lParam):
if nCode >= 0 and wParam in (WM_KEYDOWN, WM_SYSKEYDOWN):
vk = ctypes.cast(lParam, ctypes.POINTER(KBDLLHOOKSTRUCT)).contents.vkCode
alt = _pressed(VK_MENU)
ctrl = _pressed(VK_CONTROL)
if vk in (VK_LWIN, VK_RWIN):
return 1
if alt and vk in (VK_TAB, VK_F4, VK_ESCAPE, VK_SPACE):
return 1
if ctrl and vk == VK_ESCAPE:
return 1
return user32.CallNextHookEx(None, nCode, wParam, lParam)
_hook_cb = HOOKPROC(hook_proc) # keep a reference so it is not garbage collected
_hook = user32.SetWindowsHookExW(WH_KEYBOARD_LL, _hook_cb, kernel32.GetModuleHandleW(None), 0)
if not _hook:
print("WARNING: keyboard hook could not be installed - escape keys will still work")
username = getpass.getuser() or "user"
# ---------- window ----------
root = tk.Tk()
root.title("UniAuth Lock")
root.configure(bg=BG)
root.attributes("-fullscreen", True)
root.attributes("-topmost", True)
root.attributes("-alpha", 0.0) # intro fades in from transparent
root.protocol("WM_DELETE_WINDOW", lambda: None)
fams = set(tkfont.families())
def pick(*names):
for n in names:
if n in fams:
return n
return names[-1]
SANS = pick("Geist", "Inter", "Segoe UI")
MONO = pick("Geist Mono", "Cascadia Mono", "Consolas")
cv = tk.Canvas(root, bg=BG, highlightthickness=0)
cv.pack(fill="both", expand=True)
# official shield logo, 96px -> displayed at 48px for crispness
logo_img = tk.PhotoImage(data=LOGO_PNG_B64).subsample(2, 2)
entry = tk.Entry(cv, show="\u2022", justify="center",
font=(SANS, 14), bg=BG, fg=FG, insertbackground=FG,
relief="flat", highlightthickness=1,
highlightbackground=MUTED, highlightcolor=DIM)
state = {"err": False, "dot": None, "dy": 26.0, "unlocking": False, "grid": False}
sid = "SID " + "-".join("%04X" % random.randint(0, 0xFFFF) for _ in range(2))
def round_rect(x1, y1, x2, y2, r=18, **kw):
pts = [x1+r, y1, x2-r, y1, x2, y1, x2, y1+r, x2, y2-r,
x2, y2, x2-r, y2, x1+r, y2, x1, y2, x1, y2-r, x1, y1+r, x1, y1]
return cv.create_polygon(pts, smooth=True, splinesteps=36, **kw)
def layout(event=None):
w = cv.winfo_width()
h = cv.winfo_height()
if w < 10:
return
cv.delete("card")
cv.delete("chrome")
cx, cy = w / 2, h / 2
dy = state["dy"]
# static dot-grid backdrop, drawn once
if not state["grid"]:
state["grid"] = True
step = 32
for gx in range(step, w, step):
for gy in range(step, h, step):
cv.create_oval(gx - 1, gy - 1, gx + 1, gy + 1,
fill=GRID, outline="", tags="grid")
cv.tag_lower("grid")
# screen chrome
cv.create_text(36, 24, anchor="nw", text="SECURED SESSION",
font=(MONO, 9), fill=DIM, tags="chrome")
state["dot"] = cv.create_oval(24, 28, 31, 35, fill=FG, outline="", tags="chrome")
cv.create_text(w - 24, 24, anchor="ne", text=sid,
font=(MONO, 9), fill=DIM, tags="chrome")
cv.create_text(cx, h - 28, anchor="center",
text="UniAuth \u2014 One identity, access everything",
font=(MONO, 9), fill=DIM, tags="chrome")
# card
cw, ch = 400, 450
x1, y1, x2, y2 = cx - cw/2, cy - ch/2 + dy, cx + cw/2, cy + ch/2 + dy
round_rect(x1, y1, x2, y2, r=20, fill=CARD, outline=MUTED, width=1, tags="card")
# official lockup: shield image + UniAuth wordmark, centered as a group
f_word = tkfont.Font(family=SANS, size=-30, weight="bold")
sw = 48.0 # displayed logo size
gap = 14
tw = f_word.measure("UniAuth")
total = sw + gap + tw
lockup_cy = y1 + 78
shield_cx = cx - total / 2 + sw / 2
cv.create_image(shield_cx, lockup_cy, image=logo_img, tags="card")
if state["unlocking"]:
# verified-style badge on the shield's lower-right
bx, by, r = shield_cx + 17, lockup_cy + 17, 10
cv.create_oval(bx - r, by - r, bx + r, by + r, fill=FG, outline=CARD, width=2, tags="card")
cv.create_line(bx - 4.5, by + 0.5, bx - 1.0, by + 3.5, bx + 4.5, by - 4.0,
fill=CARD, width=2, capstyle="round", joinstyle="round", tags="card")
cv.create_text(cx - total / 2 + sw + gap, lockup_cy, anchor="w",
text="UniAuth", font=f_word, fill=FG, tags="card")
cv.create_text(cx, lockup_cy + 40, text=" ".join("SECURE AUTHENTICATION"),
font=(MONO, 7), fill=DIM, tags="card")
if state["unlocking"]:
cv.create_text(cx, y1 + 230, text="Welcome back",
font=(SANS, 24, "bold"), fill=FG, tags="card")
return
# live clock + date
f_clock = tkfont.Font(family=SANS, size=-50, weight="bold")
cv.create_text(cx, y1 + 176, text=time.strftime("%H:%M"),
font=f_clock, fill=FG, tags=("card", "clock"))
cv.create_text(cx, y1 + 212, text=time.strftime("%A, %d %B"),
font=(SANS, 11), fill=SUB, tags=("card", "date"))
# user line: avatar circle with initial + username
f_user = tkfont.Font(family=SANS, size=-15, weight="bold")
uw = f_user.measure(username)
atotal = 24 + 10 + uw
ax = cx - atotal / 2 + 12
ay = y1 + 256
cv.create_oval(ax - 12, ay - 12, ax + 12, ay + 12, fill=MUTED, outline="", tags="card")
cv.create_text(ax, ay, text=username[0].upper(),
font=(SANS, 10, "bold"), fill=FG, tags="card")
cv.create_text(ax + 16, ay, anchor="w", text=username,
font=f_user, fill=FG, tags="card")
cv.create_window(cx, y1 + 308, window=entry, width=290, height=42, tags="card")
cv.create_window(cx, y1 + 366, window=button, width=290, height=42, tags="card")
# caps-lock hint, right of the password field
cv.create_text(cx + 158, y1 + 308, anchor="w", text="CAPS",
font=(MONO, 8), fill=SUB, tags=("card", "caps"))
cv.itemconfig("caps", state="hidden")
# error badge (hidden until a wrong attempt)
round_rect(cx - 130, y1 + 396, cx + 130, y1 + 426, r=15,
fill=DESTR, outline="", tags=("card", "err"))
cv.create_text(cx, y1 + 411, text="Wrong password \u2014 try again",
font=(SANS, 10), fill=FG, tags=("card", "err"))
cv.itemconfig("err", state="normal" if state["err"] else "hidden")
# ---------- animations ----------
def intro(step=0):
total = 14
t = min(1.0, step / total)
e = _ease_out(t)
root.attributes("-alpha", e)
state["dy"] = 26 * (1 - e)
layout()
if t < 1.0:
root.after(24, intro, step + 1)
else:
root.attributes("-alpha", 1.0)
state["dy"] = 0.0
layout()
def finish(step=0):
total = 8
t = min(1.0, step / total)
root.attributes("-alpha", 1.0 - t)
if t < 1.0:
root.after(30, finish, step + 1)
else:
if _hook:
user32.UnhookWindowsHookEx(_hook)
root.destroy()
def shake(n=6):
if n <= 0:
layout()
return
cv.move("card", 9 if n % 2 == 0 else -9, 0)
root.after(40, shake, n - 1)
def unlock(event=None):
if state["unlocking"]:
return
if entry.get() == PASSWORD:
state["unlocking"] = True
state["err"] = False
layout()
root.after(400, finish) # hold the "Welcome back" beat, then fade out
else:
state["err"] = True
layout()
shake()
entry.delete(0, "end")
entry.focus_set()
def hide_err(event=None):
if state["err"]:
state["err"] = False
cv.itemconfig("err", state="hidden")
button = tk.Button(cv, text="Unlock \u2192", font=(SANS, 12, "bold"),
bg=FG, fg=BG, activebackground="#e5e5e5", activeforeground=BG,
relief="flat", cursor="hand2", command=unlock)
def pulse(on=True):
if state["dot"] is not None:
cv.itemconfig(state["dot"], fill=FG if on else DIM)
root.after(800, pulse, not on)
def tick():
cv.itemconfig("clock", text=time.strftime("%H:%M"))
cv.itemconfig("date", text=time.strftime("%A, %d %B"))
root.after(1000, tick)
def caps_check():
on = user32.GetKeyState(VK_CAPITAL) & 1
cv.itemconfig("caps", state="normal" if on else "hidden")
root.after(500, caps_check)
def keep_on_top():
root.attributes("-topmost", True)
root.lift()
if not state["unlocking"] and root.focus_displayof() is None:
entry.focus_set()
root.after(400, keep_on_top)
entry.bind("<Return>", unlock)
entry.bind("<Key>", hide_err)
cv.bind("<Configure>", layout)
try:
root.after(150, intro)
root.after(200, pulse)
root.after(1000, tick)
root.after(500, caps_check)
keep_on_top()
root.mainloop()
finally:
if _hook:
user32.UnhookWindowsHookEx(_hook)
if __name__ == "__main__":
try:
main()
except Exception:
try:
log = os.path.join(os.path.dirname(os.path.abspath(__file__)), "locker_error.txt")
except NameError:
log = os.path.join(os.path.expanduser("~"), "locker_error.txt")
with open(log, "w") as f:
traceback.print_exc(file=f)
raise# UniAuth Screen Locker - no admin needed
# A small tool: launches as a setup window, you choose a session password
# plus a PUBLIC hint (shown on the lock screen), then press Lock.
# Unlocking returns to the setup window so you can lock again.
# Official UniAuth shield logo embedded as base64 PNG - no external files needed.
# If anything goes wrong, a file named locker_error.txt appears next to this file.
import ctypes
import getpass
import os
import random
import time
import traceback
from ctypes import wintypes
# ---------- Official UniAuth logo (from uniauth.id/brand) ----------
# Source SVG (oauth/mark-white.svg, white shield on transparent):
UNIAUTH_MARK_SVG = '''<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none">
<path d="M12 22s8-4 8-10V5l-8-3-8 3v7c0 6 8 10 8 10z" stroke="#ffffff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>'''
# Same mark, pre-rendered to a 96x96 transparent PNG, base64-encoded:
LOGO_PNG_B64 = (
"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"
)
# ---------- UniAuth brand tokens (dark theme, from uniauth.id/press) ----------
BG = "#0a0a0a" # --background
FG = "#fafafa" # --foreground / --primary
CARD = "#171717" # neutral-900
MUTED = "#262626" # --muted / --border
SUB = "#a3a3a3" # neutral-400
DIM = "#525252" # neutral-600
GRID = "#161616" # backdrop dots, barely above background
DESTR = "#7f1d1d" # --destructive (dark)
def _ease_out(t):
return 1 - (1 - t) ** 3
def main():
import tkinter as tk
import tkinter.font as tkfont
# ---------- low-level keyboard hook ----------
# Blocks: Win keys, Alt+Tab, Alt+F4, Alt+Esc, Alt+Space, Ctrl+Esc, Ctrl+Shift+Esc
# Installed only while locked. (Ctrl+Alt+Del cannot be blocked - Windows reserves it.)
user32 = ctypes.windll.user32
kernel32 = ctypes.windll.kernel32
WH_KEYBOARD_LL = 13
WM_KEYDOWN = 0x0100
WM_SYSKEYDOWN = 0x0104
VK_TAB, VK_ESCAPE, VK_SPACE, VK_F4 = 0x09, 0x1B, 0x20, 0x73
VK_LWIN, VK_RWIN, VK_MENU, VK_CONTROL = 0x5B, 0x5C, 0x12, 0x11
VK_CAPITAL = 0x14
LRESULT = ctypes.c_longlong if ctypes.sizeof(ctypes.c_void_p) == 8 else ctypes.c_long
class KBDLLHOOKSTRUCT(ctypes.Structure):
_fields_ = [("vkCode", wintypes.DWORD),
("scanCode", wintypes.DWORD),
("flags", wintypes.DWORD),
("time", wintypes.DWORD),
("dwExtraInfo", ctypes.c_size_t)]
HOOKPROC = ctypes.WINFUNCTYPE(LRESULT, ctypes.c_int, wintypes.WPARAM, wintypes.LPARAM)
user32.SetWindowsHookExW.restype = ctypes.c_void_p
user32.SetWindowsHookExW.argtypes = [ctypes.c_int, HOOKPROC, ctypes.c_void_p, wintypes.DWORD]
user32.CallNextHookEx.restype = LRESULT
user32.CallNextHookEx.argtypes = [ctypes.c_void_p, ctypes.c_int, wintypes.WPARAM, wintypes.LPARAM]
user32.UnhookWindowsHookEx.argtypes = [ctypes.c_void_p]
user32.GetAsyncKeyState.argtypes = [ctypes.c_int]
user32.GetKeyState.argtypes = [ctypes.c_int]
kernel32.GetModuleHandleW.restype = ctypes.c_void_p
def _pressed(vk):
return user32.GetAsyncKeyState(vk) & 0x8000
def hook_proc(nCode, wParam, lParam):
if nCode >= 0 and wParam in (WM_KEYDOWN, WM_SYSKEYDOWN):
vk = ctypes.cast(lParam, ctypes.POINTER(KBDLLHOOKSTRUCT)).contents.vkCode
alt = _pressed(VK_MENU)
ctrl = _pressed(VK_CONTROL)
if vk in (VK_LWIN, VK_RWIN):
return 1
if alt and vk in (VK_TAB, VK_F4, VK_ESCAPE, VK_SPACE):
return 1
if ctrl and vk == VK_ESCAPE:
return 1
return user32.CallNextHookEx(None, nCode, wParam, lParam)
_hook_cb = HOOKPROC(hook_proc) # keep a reference so it is not garbage collected
state = {"err": False, "dot": None, "dy": 26.0, "unlocking": False,
"grid": False, "locked": False, "hook": None,
"password": "", "hint": ""}
def set_hook(on):
if on and not state["hook"]:
state["hook"] = user32.SetWindowsHookExW(WH_KEYBOARD_LL, _hook_cb,
kernel32.GetModuleHandleW(None), 0)
if not state["hook"]:
print("WARNING: keyboard hook failed - escape keys will still work")
elif not on and state["hook"]:
user32.UnhookWindowsHookEx(state["hook"])
state["hook"] = None
username = getpass.getuser() or "user"
# ---------- window ----------
root = tk.Tk()
root.title("UniAuth Session Locker")
root.configure(bg=BG)
root.minsize(400, 560)
fams = set(tkfont.families())
def pick(*names):
for n in names:
if n in fams:
return n
return names[-1]
SANS = pick("Geist", "Inter", "Segoe UI")
MONO = pick("Geist Mono", "Cascadia Mono", "Consolas")
logo48 = tk.PhotoImage(data=LOGO_PNG_B64).subsample(2, 2) # 48px, lock screen
logo32 = tk.PhotoImage(data=LOGO_PNG_B64).subsample(3, 3) # 32px, setup window
sid = "SID " + "-".join("%04X" % random.randint(0, 0xFFFF) for _ in range(2))
# ================= setup screen (windowed tool) =================
setup = tk.Frame(root, bg=BG)
tk.Label(setup, image=logo32, bg=BG).pack(pady=(30, 6))
tk.Label(setup, text="UniAuth", font=(SANS, 16, "bold"), fg=FG, bg=BG).pack()
tk.Label(setup, text=" ".join("SESSION LOCKER"), font=(MONO, 7), fg=DIM, bg=BG).pack(pady=(2, 18))
tk.Label(setup, text="Set up your lock", font=(SANS, 18, "bold"), fg=FG, bg=BG).pack()
tk.Label(setup, text="The password protects this lock session only.",
font=(SANS, 10), fg=SUB, bg=BG).pack(pady=(2, 16))
form = tk.Frame(setup, bg=BG)
form.pack()
tk.Label(form, text="PASSWORD", font=(MONO, 8), fg=DIM, bg=BG, anchor="w").pack(fill="x")
pw_entry = tk.Entry(form, show="\u2022", font=(SANS, 13), width=30,
bg=BG, fg=FG, insertbackground=FG, relief="flat",
highlightthickness=1, highlightbackground=MUTED, highlightcolor=DIM)
pw_entry.pack(ipady=8, pady=(4, 14))
tk.Label(form, text="HINT (PUBLIC)", font=(MONO, 8), fg=DIM, bg=BG, anchor="w").pack(fill="x")
hint_entry = tk.Entry(form, font=(SANS, 13), width=30,
bg=BG, fg=FG, insertbackground=FG, relief="flat",
highlightthickness=1, highlightbackground=MUTED, highlightcolor=DIM)
hint_entry.pack(ipady=8, pady=(4, 2))
tk.Label(form, text="Shown on the lock screen \u2014 never type your password here.",
font=(SANS, 9), fg=SUB, bg=BG, anchor="w", wraplength=300, justify="left").pack(fill="x")
setup_err = tk.Label(setup, text="", font=(SANS, 9), fg=FG, bg=DESTR,
padx=10, pady=4)
# packed on demand
tk.Button(setup, text="Lock \u2192", font=(SANS, 12, "bold"),
bg=FG, fg=BG, activebackground="#e5e5e5", activeforeground=BG,
relief="flat", cursor="hand2", width=24,
command=lambda: enter_lock()).pack(pady=(18, 0), ipady=8)
hint_entry.bind("<Return>", lambda e: enter_lock())
# ================= lock screen (fullscreen) =================
cv = tk.Canvas(root, bg=BG, highlightthickness=0)
entry = tk.Entry(cv, show="\u2022", justify="center",
font=(SANS, 14), bg=BG, fg=FG, insertbackground=FG,
relief="flat", highlightthickness=1,
highlightbackground=MUTED, highlightcolor=DIM)
def round_rect(x1, y1, x2, y2, r=18, **kw):
pts = [x1+r, y1, x2-r, y1, x2, y1, x2, y1+r, x2, y2-r,
x2, y2, x2-r, y2, x1+r, y2, x1, y2, x1, y2-r, x1, y1+r, x1, y1]
return cv.create_polygon(pts, smooth=True, splinesteps=36, **kw)
def layout(event=None):
w = cv.winfo_width()
h = cv.winfo_height()
if w < 10:
return
cv.delete("card")
cv.delete("chrome")
cx, cy = w / 2, h / 2
dy = state["dy"]
# static dot-grid backdrop, drawn once
if not state["grid"]:
state["grid"] = True
step = 32
for gx in range(step, w, step):
for gy in range(step, h, step):
cv.create_oval(gx - 1, gy - 1, gx + 1, gy + 1,
fill=GRID, outline="", tags="grid")
cv.tag_lower("grid")
# screen chrome
cv.create_text(36, 24, anchor="nw", text="SECURED SESSION",
font=(MONO, 9), fill=DIM, tags="chrome")
state["dot"] = cv.create_oval(24, 28, 31, 35, fill=FG, outline="", tags="chrome")
cv.create_text(w - 24, 24, anchor="ne", text=sid,
font=(MONO, 9), fill=DIM, tags="chrome")
cv.create_text(cx, h - 28, anchor="center",
text="UniAuth \u2014 One identity, access everything",
font=(MONO, 9), fill=DIM, tags="chrome")
# card
cw, ch = 400, 470
x1, y1, x2, y2 = cx - cw/2, cy - ch/2 + dy, cx + cw/2, cy + ch/2 + dy
round_rect(x1, y1, x2, y2, r=20, fill=CARD, outline=MUTED, width=1, tags="card")
# official lockup: shield image + UniAuth wordmark, centered as a group
f_word = tkfont.Font(family=SANS, size=-30, weight="bold")
sw = 48.0
gap = 14
tw = f_word.measure("UniAuth")
total = sw + gap + tw
lockup_cy = y1 + 78
shield_cx = cx - total / 2 + sw / 2
cv.create_image(shield_cx, lockup_cy, image=logo48, tags="card")
if state["unlocking"]:
bx, by, r = shield_cx + 17, lockup_cy + 17, 10
cv.create_oval(bx - r, by - r, bx + r, by + r, fill=FG, outline=CARD, width=2, tags="card")
cv.create_line(bx - 4.5, by + 0.5, bx - 1.0, by + 3.5, bx + 4.5, by - 4.0,
fill=CARD, width=2, capstyle="round", joinstyle="round", tags="card")
cv.create_text(cx - total / 2 + sw + gap, lockup_cy, anchor="w",
text="UniAuth", font=f_word, fill=FG, tags="card")
cv.create_text(cx, lockup_cy + 40, text=" ".join("SECURE AUTHENTICATION"),
font=(MONO, 7), fill=DIM, tags="card")
if state["unlocking"]:
cv.create_text(cx, y1 + 240, text="Welcome back",
font=(SANS, 24, "bold"), fill=FG, tags="card")
return
# live clock + date
f_clock = tkfont.Font(family=SANS, size=-50, weight="bold")
cv.create_text(cx, y1 + 174, text=time.strftime("%H:%M"),
font=f_clock, fill=FG, tags=("card", "clock"))
cv.create_text(cx, y1 + 210, text=time.strftime("%A, %d %B"),
font=(SANS, 11), fill=SUB, tags=("card", "date"))
# user line: avatar circle with initial + username
f_user = tkfont.Font(family=SANS, size=-15, weight="bold")
uw = f_user.measure(username)
atotal = 24 + 10 + uw
ax = cx - atotal / 2 + 12
ay = y1 + 254
cv.create_oval(ax - 12, ay - 12, ax + 12, ay + 12, fill=MUTED, outline="", tags="card")
cv.create_text(ax, ay, text=username[0].upper(),
font=(SANS, 10, "bold"), fill=FG, tags="card")
cv.create_text(ax + 16, ay, anchor="w", text=username,
font=f_user, fill=FG, tags="card")
# public hint (only if one was set)
if state["hint"]:
cv.create_text(cx, y1 + 284, text="Hint: " + state["hint"],
font=(SANS, 10), fill=SUB, tags="card")
cv.create_window(cx, y1 + 318, window=entry, width=290, height=42, tags="card")
cv.create_window(cx, y1 + 376, window=button, width=290, height=42, tags="card")
# caps-lock hint, right of the password field
cv.create_text(cx + 158, y1 + 318, anchor="w", text="CAPS",
font=(MONO, 8), fill=SUB, tags=("card", "caps"))
cv.itemconfig("caps", state="hidden")
# error badge (hidden until a wrong attempt)
round_rect(cx - 130, y1 + 402, cx + 130, y1 + 432, r=15,
fill=DESTR, outline="", tags=("card", "err"))
cv.create_text(cx, y1 + 417, text="Wrong password \u2014 try again",
font=(SANS, 10), fill=FG, tags=("card", "err"))
cv.itemconfig("err", state="normal" if state["err"] else "hidden")
# ---------- mode switching ----------
def enter_lock():
pw = pw_entry.get()
if not pw:
setup_err.config(text="Set a password first")
setup_err.pack(pady=(12, 0))
pw_entry.focus_set()
return
setup_err.pack_forget()
state["password"] = pw
state["hint"] = hint_entry.get().strip()
state["locked"] = True
state["unlocking"] = False
state["err"] = False
state["dy"] = 26.0
entry.delete(0, "end")
setup.pack_forget()
cv.pack(fill="both", expand=True)
root.attributes("-fullscreen", True)
root.attributes("-topmost", True)
root.attributes("-alpha", 0.0)
set_hook(True)
root.after(80, intro)
def exit_lock():
set_hook(False)
state["locked"] = False
state["unlocking"] = False
state["err"] = False
entry.delete(0, "end")
root.attributes("-alpha", 1.0)
root.attributes("-topmost", False)
root.attributes("-fullscreen", False)
cv.pack_forget()
sw = root.winfo_screenwidth()
sh = root.winfo_screenheight()
root.geometry("440x600+%d+%d" % ((sw - 440) // 2, (sh - 600) // 2))
setup.pack(fill="both", expand=True)
pw_entry.focus_set()
# ---------- animations ----------
def intro(step=0):
total = 14
t = min(1.0, step / total)
e = _ease_out(t)
root.attributes("-alpha", e)
state["dy"] = 26 * (1 - e)
layout()
if t < 1.0:
root.after(24, intro, step + 1)
else:
root.attributes("-alpha", 1.0)
state["dy"] = 0.0
layout()
def finish(step=0):
total = 8
t = min(1.0, step / total)
root.attributes("-alpha", 1.0 - t)
if t < 1.0:
root.after(30, finish, step + 1)
else:
exit_lock()
def shake(n=6):
if n <= 0:
layout()
return
cv.move("card", 9 if n % 2 == 0 else -9, 0)
root.after(40, shake, n - 1)
def unlock(event=None):
if state["unlocking"]:
return
if entry.get() == state["password"]:
state["unlocking"] = True
state["err"] = False
layout()
root.after(400, finish) # hold the "Welcome back" beat, then fade out
else:
state["err"] = True
layout()
shake()
entry.delete(0, "end")
entry.focus_set()
def hide_err(event=None):
if state["err"]:
state["err"] = False
cv.itemconfig("err", state="hidden")
button = tk.Button(cv, text="Unlock \u2192", font=(SANS, 12, "bold"),
bg=FG, fg=BG, activebackground="#e5e5e5", activeforeground=BG,
relief="flat", cursor="hand2", command=unlock)
# ---------- background loops (active only while locked) ----------
def pulse(on=True):
if state["locked"] and state["dot"] is not None:
cv.itemconfig(state["dot"], fill=FG if on else DIM)
root.after(800, pulse, not on)
def tick():
if state["locked"]:
cv.itemconfig("clock", text=time.strftime("%H:%M"))
cv.itemconfig("date", text=time.strftime("%A, %d %B"))
root.after(1000, tick)
def caps_check():
if state["locked"]:
on = user32.GetKeyState(VK_CAPITAL) & 1
cv.itemconfig("caps", state="normal" if on else "hidden")
root.after(500, caps_check)
def keep_on_top():
if state["locked"]:
root.attributes("-topmost", True)
root.lift()
if not state["unlocking"] and root.focus_displayof() is None:
entry.focus_set()
root.after(400, keep_on_top)
def on_close():
if not state["locked"]:
root.destroy()
entry.bind("<Return>", unlock)
entry.bind("<Key>", hide_err)
cv.bind("<Configure>", layout)
root.protocol("WM_DELETE_WINDOW", on_close)
try:
sw = root.winfo_screenwidth()
sh = root.winfo_screenheight()
root.geometry("440x600+%d+%d" % ((sw - 440) // 2, (sh - 600) // 2))
setup.pack(fill="both", expand=True)
pw_entry.focus_set()
root.after(800, pulse)
root.after(1000, tick)
root.after(500, caps_check)
root.after(400, keep_on_top)
root.mainloop()
finally:
set_hook(False)
if __name__ == "__main__":
try:
main()
except Exception:
try:
log = os.path.join(os.path.dirname(os.path.abspath(__file__)), "locker_error.txt")
except NameError:
log = os.path.join(os.path.expanduser("~"), "locker_error.txt")
with open(log, "w") as f:
traceback.print_exc(file=f)
raiseSign out, and the theater row
Signing out passes every test and works on every image: your session simply ends. Its cost is that it closes every app and every half-finished thing. For anything genuinely sensitive it is the correct answer anyway — overkill for a toilet run, right for everything longer.
And at the bottom: the monitor's power button. Instant black screen, zero protection, the computing equivalent of pulling a blanket over your head.
Everything, ranked
| Method | Real Windows lock? | On our managed image | Notes |
|---|---|---|---|
| Win+L / Ctrl+Alt+Del → Lock / Start menu → Lock | Yes | Removed by policy | The normal ways — all gone on this machine |
rundll32.exe user32.dll,LockWorkStation | Yes | Did nothing | Calls the real lock API directly — often bypasses disabled menus, but not a hardened image |
| Secure screensaver trick | Sort of | Dark screen, no password | The "require password" flag is only loaded at logon, so mid-session changes never apply |
tsdiscon | Yes | Works | Disconnects to the sign-in screen; apps keep running; real password required. Only flaw: the occasional invisible-mouse quirk after signing back in — fix with Ctrl+Shift+Esc, then Esc |
| Screen-locker overlay (our tool) | No — custom overlay | Works | Fullscreen lock with your own session password and hint; blocks Win/Alt+Tab/etc. Escapable via Ctrl+Alt+Del → Sign out, so it stops classmates, not determined attackers |
| Sign out | Yes | Works | Guaranteed, but closes all your apps |
| Monitor power button | No | Works | Instant black screen, zero protection — pure theater |
What we would actually do
- Toilet runs and coffee: the overlay tool. Nicer to look at, your own password and hint, no mouse quirk.
- Away for a while, or anything you actually care about: the
tsdisconbatch file. It is the real Windows sign-in screen, unbypassable without your account password — just do the Ctrl+Shift+Esc flick after logging back in if the pointer hides. - Anything sensitive on the screen: sign out. Yes, it closes everything. That is the point.
What we verified, and what you should check
Every row above was tried on one deliberately locked-down school/work image. Two results depend on your machine and you should test them before you trust them: LockWorkStation (blocked here, alive on many others) and the screensaver flag (inert mid-session here). tsdiscon and Sign out are session features rather than policy surface, so they are the rows we would expect to survive anywhere — but test them once, calmly, before the moment you need them. And if your organization disabled locking deliberately rather than accidentally, take this list to your IT department instead of deploying it yourself: the goal is a locked screen, not a policy dispute.