-
Notifications
You must be signed in to change notification settings - Fork 2.8k
luci-app-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
base: master
Are you sure you want to change the base?
Changes from all commits
f91d3eb
6dd1bea
2bb8966
2faafc4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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-app-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-app-2fa | ||
|
|
||
| include ../../luci.mk | ||
|
|
||
| # call BuildPackage - OpenWrt buildroot | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,176 @@ | ||
| msgid "" | ||
| msgstr "Content-Type: text/plain; charset=UTF-8" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:149 | ||
| msgid "2FA enabled" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:23 | ||
| msgid "" | ||
| "Adds TOTP/HOTP verification as an additional authentication factor for LuCI " | ||
| "login." | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:110 | ||
| msgid "Advanced" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:113 | ||
| msgid "Allow bypassing 2FA from trusted IP addresses." | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:15 | ||
| msgid "Authentication" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:48 | ||
| msgid "" | ||
| "Base32-encoded secret key for TOTP/HOTP. Generate using an authenticator app." | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:29 | ||
| msgid "Basic Settings" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:105 | ||
| msgid "" | ||
| "Block remote access when system time is not calibrated. LAN access is still " | ||
| "allowed." | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:38 | ||
| msgid "" | ||
| "Configure 2FA keys for individual users. The key must be a Base32-encoded " | ||
| "secret." | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:31 | ||
| msgid "Enable 2FA" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:112 | ||
| msgid "Enable IP Whitelist" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:78 | ||
| msgid "Enable Rate Limiting" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:32 | ||
| msgid "Enable two-factor authentication for LuCI login." | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:65 | ||
| msgid "HOTP (Counter-based)" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:98 | ||
| msgid "How long to lock out after too many failed attempts." | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:118 | ||
| msgid "IP addresses or CIDR ranges that bypass 2FA. Example: 192.168.1.0/24" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:144 | ||
| msgid "IP whitelist on" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:57 | ||
| msgid "Invalid Base32 format. Use only A-Z and 2-7 characters." | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:79 | ||
| msgid "Limit failed OTP attempts to prevent brute-force attacks." | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:97 | ||
| msgid "Lockout Duration (seconds)" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:18 | ||
| msgid "Login" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:83 | ||
| msgid "Max Failed Attempts" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:84 | ||
| msgid "Maximum failed attempts before lockout." | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:123 | ||
| msgid "Minimum Valid Time" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:61 | ||
| msgid "OTP Type for root" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:90 | ||
| msgid "Rate Limit Window (seconds)" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:47 | ||
| msgid "Secret Key for root" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:76 | ||
| msgid "Security" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:104 | ||
| msgid "Strict Mode" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:64 | ||
| msgid "TOTP (Time-based)" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:62 | ||
| msgid "" | ||
| "TOTP (Time-based) is recommended. HOTP (Counter-based) is for special cases." | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:68 | ||
| msgid "TOTP Time Step" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:69 | ||
| msgid "Time step in seconds for TOTP. Default is 30 seconds." | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:91 | ||
| msgid "Time window for counting failed attempts." | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:22 | ||
| msgid "Two-Factor Authentication" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:124 | ||
| msgid "" | ||
| "Unix timestamp before which system time is considered uncalibrated. Default: " | ||
| "2026-01-01." | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:37 | ||
| msgid "User Configuration" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:117 | ||
| msgid "Whitelisted IPs" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:141 | ||
| msgid "rate limiting on" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:138 | ||
| msgid "root user configured" | ||
| msgstr "" | ||
|
|
||
| #: applications/luci-app-2fa/root/www/luci-static/resources/view/plugins/bb4ea47fcffb44ec9bb3d3673c9b4ed2.js:147 | ||
| msgid "strict mode" | ||
| msgstr "" |
| 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 |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,133 @@ | ||
| #!/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'; | ||
|
|
||
| function hex_to_bin(hex) { | ||
| let bin = ""; | ||
| for (let i = 0; i < length(hex); i += 2) { | ||
| bin += chr(int(substr(hex, i, 2), 16)); | ||
| } | ||
| return bin; | ||
| } | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. |
||
|
|
||
| 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 = hex_to_bin(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 = hex_to_bin(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(">I", 0) + pack(">I", counter_int); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I wonder whether it's simpler to do: pack(">Q", counter_int)It should work the same I think. ( The RFC 4226 and 6238 use a single 8 byte integer ) |
||
|
|
||
| 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; | ||
|
Comment on lines
+70
to
+71
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we get the same result if we do like the RFC?: offset = hmac[19] & 0xf;
binary_code =
((hmac[offset] & 0x7f) << 24) |
(hmac[offset+1] << 16) |
(hmac[offset+2] << 8) |
(hmac[offset+3]);Need to hexdec sha1 output. |
||
|
|
||
| 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); | ||
Uh oh!
There was an error while loading. Please reload this page.