-
Notifications
You must be signed in to change notification settings - Fork 2.8k
luci-plugin-2fa: init checkin #8280
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
systemcrash
merged 1 commit into
openwrt:master
from
Tokisaki-Galaxy:tokisaki-luci-app-otp
Apr 7, 2026
Merged
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,23 @@ | ||
| # | ||
| # Copyright (C) 2026 tokisaki galaxy <moebest@outlook.jp> | ||
| # Copyright (C) 2024 Christian Marangi <ansuelsmth@gmail.com> | ||
| # | ||
| # This is free software, licensed under the Apache License, Version 2.0. | ||
| # | ||
|
|
||
| include $(TOPDIR)/rules.mk | ||
|
|
||
| PKG_NAME:=luci-plugin-2fa | ||
|
|
||
| PKG_LICENSE:=Apache-2.0 | ||
| PKG_MAINTAINER:=tokisaki galaxy <moebest@outlook.jp> | ||
| PKG_DESCRIPTION:=LuCI 2-Factor Authentication Plugin | ||
|
|
||
| LUCI_TITLE:=LuCI 2-Factor Authentication | ||
| LUCI_DEPENDS:=+luci-base +luci-lib-uqr +ucode-mod-struct +ucode-mod-digest +ucode-mod-log | ||
| LUCI_PKGARCH:=all | ||
| LUCI_URL:=https://github.com/tokisaki-galaxy/luci-plugin-2fa | ||
|
|
||
| include ../../luci.mk | ||
|
|
||
| # call BuildPackage - OpenWrt buildroot |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,196 @@ | ||
| msgid "" | ||
| msgstr "Content-Type: text/plain; charset=UTF-8" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:189 | ||
| msgid "2FA enabled" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:53 | ||
| msgid "" | ||
| "Adds TOTP/HOTP verification as an additional authentication factor for LuCI " | ||
| "login." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:150 | ||
| msgid "Advanced" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:153 | ||
| msgid "Allow bypassing 2FA from trusted IP addresses." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:45 | ||
| msgid "Authentication" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:112 | ||
| msgid "Authenticator QR Code" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:85 | ||
| msgid "" | ||
| "Base32-encoded secret key for TOTP/HOTP. Generate using an authenticator app." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:59 | ||
| msgid "Basic Settings" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:145 | ||
| msgid "" | ||
| "Block remote access when system time is not calibrated. LAN access is still " | ||
| "allowed." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:75 | ||
| msgid "" | ||
| "Configure 2FA keys for individual users. The key must be a Base32-encoded " | ||
| "secret." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:61 | ||
| msgid "Enable 2FA" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:152 | ||
| msgid "Enable IP Whitelist" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:118 | ||
| msgid "Enable Rate Limiting" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:62 | ||
| msgid "Enable two-factor authentication for LuCI login." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:67 | ||
| msgid "Execution order for this plugin. Lower values run earlier." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:102 | ||
| msgid "HOTP (Counter-based)" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:138 | ||
| msgid "How long to lock out after too many failed attempts." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:158 | ||
| msgid "IP addresses or CIDR ranges that bypass 2FA. Example: 192.168.1.0/24" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:184 | ||
| msgid "IP whitelist on" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:94 | ||
| msgid "Invalid Base32 format. Use only A-Z and 2-7 characters." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:119 | ||
| msgid "Limit failed OTP attempts to prevent brute-force attacks." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:137 | ||
| msgid "Lockout Duration (seconds)" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:48 | ||
| msgid "Login" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:123 | ||
| msgid "Max Failed Attempts" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:124 | ||
| msgid "Maximum failed attempts before lockout." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:163 | ||
| msgid "Minimum Valid Time" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:98 | ||
| msgid "OTP Type for root" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:66 | ||
| msgid "Priority" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:130 | ||
| msgid "Rate Limit Window (seconds)" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:36 | ||
| msgid "Scan this QR code with your authenticator app." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:84 | ||
| msgid "Secret Key for root" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:116 | ||
| msgid "Security" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:14 | ||
| msgid "Set and save the secret key first to display a QR code." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:144 | ||
| msgid "Strict Mode" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:101 | ||
| msgid "TOTP (Time-based)" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:99 | ||
| msgid "" | ||
| "TOTP (Time-based) is recommended. HOTP (Counter-based) is for special cases." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:105 | ||
| msgid "TOTP Time Step" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:106 | ||
| msgid "Time step in seconds for TOTP. Default is 30 seconds." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:131 | ||
| msgid "Time window for counting failed attempts." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:52 | ||
| msgid "Two-Factor Authentication" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:164 | ||
| msgid "" | ||
| "Unix timestamp before which system time is considered uncalibrated. Default: " | ||
| "2026-01-01." | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:74 | ||
| msgid "User Configuration" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:157 | ||
| msgid "Whitelisted IPs" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:181 | ||
| msgid "rate limiting on" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:178 | ||
| msgid "root user configured" | ||
| msgstr "" | ||
|
|
||
| #: plugins/luci-plugin-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:187 | ||
| msgid "strict mode" | ||
| msgstr "" |
44 changes: 44 additions & 0 deletions
44
plugins/luci-plugin-2fa/root/etc/uci-defaults/luci-app-2fa
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,44 @@ | ||
| #!/bin/sh | ||
|
|
||
| # luci-app-2fa: Setup script for two-factor authentication plugin | ||
| # This script sets up the 2FA plugin configuration in luci_plugins | ||
|
|
||
| PLUGIN_UUID="bb4ea47fcffb44ec9bb3d3673c9b4ed2" | ||
|
|
||
| # Ensure luci_plugins config file exists | ||
| touch /etc/config/luci_plugins | ||
|
|
||
| # Create global section if not exists | ||
| uci -q get luci_plugins.global >/dev/null || { | ||
| uci set luci_plugins.global=global | ||
| uci set luci_plugins.global.enabled='0' | ||
| } | ||
|
|
||
| # Enable auth_login plugins class if not set | ||
| uci -q get luci_plugins.global.auth_login_enabled >/dev/null || { | ||
| uci set luci_plugins.global.auth_login_enabled='0' | ||
| } | ||
|
|
||
| # Create 2FA plugin section if not exists | ||
| uci -q get "luci_plugins.${PLUGIN_UUID}" >/dev/null || { | ||
| uci set "luci_plugins.${PLUGIN_UUID}=auth_login" | ||
| uci set "luci_plugins.${PLUGIN_UUID}.enabled=0" | ||
| uci set "luci_plugins.${PLUGIN_UUID}.name=Two-Factor Authentication" | ||
|
|
||
| # Rate limiting defaults | ||
| uci set "luci_plugins.${PLUGIN_UUID}.rate_limit_enabled=1" | ||
| uci set "luci_plugins.${PLUGIN_UUID}.rate_limit_max_attempts=5" | ||
| uci set "luci_plugins.${PLUGIN_UUID}.rate_limit_window=60" | ||
| uci set "luci_plugins.${PLUGIN_UUID}.rate_limit_lockout=300" | ||
|
|
||
| # Security defaults | ||
| uci set "luci_plugins.${PLUGIN_UUID}.strict_mode=0" | ||
| uci set "luci_plugins.${PLUGIN_UUID}.ip_whitelist_enabled=0" | ||
|
|
||
| # Time calibration threshold (2026-01-01 00:00:00 UTC) | ||
| uci set "luci_plugins.${PLUGIN_UUID}.min_valid_time=1767225600" | ||
| } | ||
|
|
||
| uci commit luci_plugins | ||
|
|
||
| exit 0 |
125 changes: 125 additions & 0 deletions
125
plugins/luci-plugin-2fa/root/usr/libexec/generate_otp.uc
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| #!/usr/bin/ucode | ||
|
|
||
| // Copyright (c) 2024 Christian Marangi <ansuelsmth@gmail.com> | ||
| // Copyright (c) 2026 tokiskai galaxy <moebest@outlook.jp> | ||
| import { cursor } from 'uci'; | ||
| import { sha1 } from 'digest'; | ||
| import { pack } from 'struct'; | ||
|
|
||
| const base32_decode_table = (function() { | ||
| let t = {}; | ||
| for (let i = 0; i < 26; i++) { t[ord('A') + i] = i; t[ord('a') + i] = i; } | ||
| for (let i = 0; i < 6; i++) { t[ord('2') + i] = 26 + i; } | ||
| return t; | ||
| })(); | ||
|
|
||
| function decode_base32_to_bin(string) { | ||
| let clean = replace(string, /[\s=]/g, ""); | ||
| if (length(clean) == 0) return null; | ||
|
|
||
| let bin = ""; | ||
| let buffer = 0; | ||
| let bits = 0; | ||
|
|
||
| for (let i = 0; i < length(clean); i++) { | ||
| let val = base32_decode_table[ord(clean, i)]; | ||
| if (val === null || val === undefined) continue; | ||
|
|
||
| buffer = (buffer << 5) | val; | ||
| bits += 5; | ||
|
|
||
| if (bits >= 8) { | ||
| bits -= 8; | ||
| bin += chr((buffer >> bits) & 0xff); | ||
| } | ||
| } | ||
| return bin; | ||
| } | ||
|
|
||
| function calculate_hmac_sha1(key, message) { | ||
| const blocksize = 64; | ||
| if (length(key) > blocksize) key = hexdec(sha1(key)); | ||
| while (length(key) < blocksize) key += chr(0); | ||
|
|
||
| let o_key_pad = "", i_key_pad = ""; | ||
| for (let i = 0; i < blocksize; i++) { | ||
| let k = ord(key, i); | ||
| o_key_pad += chr(k ^ 0x5c); | ||
| i_key_pad += chr(k ^ 0x36); | ||
| } | ||
| let inner_hash = hexdec(sha1(i_key_pad + message)); | ||
| return sha1(o_key_pad + inner_hash); | ||
| } | ||
|
|
||
| function calculate_otp(secret_base32, counter_int) { | ||
| let secret_bin = decode_base32_to_bin(secret_base32); | ||
| if (!secret_bin) return null; | ||
|
|
||
| let counter_bin = pack(">Q", counter_int); | ||
|
|
||
| let hmac_hex = calculate_hmac_sha1(secret_bin, counter_bin); | ||
|
|
||
| let offset = int(substr(hmac_hex, 38, 2), 16) & 0xf; | ||
| let binary_code = int(substr(hmac_hex, offset * 2, 8), 16) & 0x7fffffff; | ||
|
|
||
| return sprintf("%06d", binary_code % 1000000); | ||
| } | ||
|
|
||
| let username = ARGV[0]; | ||
| let no_increment = false; | ||
| let custom_time = null; | ||
| let plugin_uuid = null; | ||
|
|
||
| for (let i = 1; i < length(ARGV); i++) { | ||
| let arg = ARGV[i]; | ||
| if (arg == '--no-increment') { | ||
| no_increment = true; | ||
| } else if (substr(arg, 0, 7) == '--time=') { | ||
| let time_str = substr(arg, 7); | ||
| if (match(time_str, /^[0-9]+$/)) { | ||
| custom_time = int(time_str); | ||
| if (custom_time < 946684800 || custom_time > 4102444800) custom_time = null; | ||
| } | ||
| } else if (substr(arg, 0, 9) == '--plugin=') { | ||
| let uuid_str = substr(arg, 9); | ||
| if (match(uuid_str, /^[0-9a-fA-F]{32}$/)) plugin_uuid = uuid_str; | ||
| } | ||
| } | ||
|
|
||
| if (!username || username == '') exit(1); | ||
|
|
||
| let ctx = cursor(); | ||
| let otp_type, secret, counter, step; | ||
|
|
||
| if (plugin_uuid) { | ||
| otp_type = ctx.get('luci_plugins', plugin_uuid, 'type_' + username) || 'totp'; | ||
| secret = ctx.get('luci_plugins', plugin_uuid, 'key_' + username); | ||
| counter = int(ctx.get('luci_plugins', plugin_uuid, 'counter_' + username) || '0'); | ||
| step = int(ctx.get('luci_plugins', plugin_uuid, 'step_' + username) || '30'); | ||
| } else { | ||
| otp_type = ctx.get('2fa', username, 'type') || 'totp'; | ||
| secret = ctx.get('2fa', username, 'key'); | ||
| counter = int(ctx.get('2fa', username, 'counter') || '0'); | ||
| step = int(ctx.get('2fa', username, 'step') || '30'); | ||
| } | ||
|
|
||
| if (!secret) exit(1); | ||
|
|
||
| let otp; | ||
| if (otp_type == 'hotp') { | ||
| otp = calculate_otp(secret, counter); | ||
| if (!no_increment && otp) { | ||
| if (plugin_uuid) { | ||
| ctx.set('luci_plugins', plugin_uuid, 'counter_' + username, '' + (counter + 1)); | ||
| ctx.commit('luci_plugins'); | ||
| } else { | ||
| ctx.set('2fa', username, 'counter', '' + (counter + 1)); | ||
| ctx.commit('2fa'); | ||
| } | ||
| } | ||
| } else { | ||
| let timestamp = (custom_time != null) ? custom_time : time(); | ||
| otp = calculate_otp(secret, int(timestamp / step)); | ||
| } | ||
|
|
||
| if (otp) print(otp); else exit(1); | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.