diff --git a/modules/luci-base/htdocs/luci-static/resources/uqr.js b/modules/luci-base/htdocs/luci-static/resources/uqr.js new file mode 100644 index 000000000000..00d7a3d7b354 --- /dev/null +++ b/modules/luci-base/htdocs/luci-static/resources/uqr.js @@ -0,0 +1,33 @@ +// MIT License + +// Copyright (c) Project Nayuki +// Copyright (c) 2023 Anthony Fu +// Minified and modified/stripped of not useful components +'use strict'; +var QrCodeDataType=(t=>(t[t.Border=-1]="Border",t[t.Data=0]="Data",t[t.Function=1]="Function",t[t.Position=2]="Position",t[t.Timing=3]="Timing",t[t.Alignment=4]="Alignment",t))(QrCodeDataType||{}),__defProp=Object.defineProperty,__defNormalProp=(t,e,n)=>e in t?__defProp(t,e,{enumerable:!0,configurable:!0,writable:!0,value:n}):t[e]=n,__publicField=(t,e,n)=>(__defNormalProp(t,"symbol"!=typeof e?e+"":e,n),n);const LOW=[0,1],MEDIUM=[1,0],QUARTILE=[2,3],HIGH=[3,2],EccMap={L:LOW,M:MEDIUM,Q:QUARTILE,H:HIGH},NUMERIC_REGEX=/^[0-9]*$/,ALPHANUMERIC_REGEX=/^[A-Z0-9 $%*+.\/:-]*$/,ALPHANUMERIC_CHARSET="0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:",MIN_VERSION=1,MAX_VERSION=40,PENALTY_N1=3,PENALTY_N2=3,PENALTY_N3=40,PENALTY_N4=10,ECC_CODEWORDS_PER_BLOCK=[[-1,7,10,15,20,26,18,20,24,30,18,20,24,26,30,22,24,28,30,28,28,28,28,30,30,26,28,30,30,30,30,30,30,30,30,30,30,30,30,30,30],[-1,10,16,26,18,24,16,18,22,22,26,30,22,22,24,24,28,28,26,26,26,26,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28,28],[-1,13,22,18,26,18,24,18,22,20,24,28,26,24,20,30,24,28,28,26,30,28,30,30,30,30,28,30,30,30,30,30,30,30,30,30,30,30,30,30,30],[-1,17,28,22,16,22,28,26,26,24,28,24,28,22,24,24,30,28,28,26,28,30,24,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30,30]],NUM_ERROR_CORRECTION_BLOCKS=[[-1,1,1,1,1,1,2,2,2,2,4,4,4,4,4,6,6,6,6,7,8,8,9,9,10,12,12,12,13,14,15,16,17,18,19,19,20,21,22,24,25],[-1,1,1,1,2,2,4,4,4,5,5,5,8,9,9,10,10,11,13,14,16,17,17,18,20,21,23,25,26,28,29,31,33,35,37,38,40,43,45,47,49],[-1,1,1,2,2,4,4,6,6,8,8,8,10,12,16,12,17,16,18,21,20,23,23,25,27,29,34,34,35,38,40,43,45,48,51,53,56,59,62,65,68],[-1,1,1,2,4,4,4,5,6,8,8,11,11,16,16,18,16,19,21,25,25,25,34,30,32,35,37,40,42,45,48,51,54,57,60,63,66,70,74,77,81]];class QrCode{constructor(t,e,n,r){if(this.version=t,this.ecc=e,__publicField(this,"size"),__publicField(this,"mask"),__publicField(this,"modules",[]),__publicField(this,"types",[]),tMAX_VERSION)throw new RangeError("Version value out of range");if(r<-1||r>7)throw new RangeError("Mask value out of range");this.size=4*t+17;const o=Array.from({length:this.size},(()=>!1));for(let t=0;t0)));this.drawFunctionPatterns();const s=this.addEccAndInterleave(n);if(this.drawCodewords(s),-1===r){let t=1e9;for(let e=0;e<8;e++){this.applyMask(e),this.drawFormatBits(e);const n=this.getPenaltyScore();n=0&&t=0&&e>>9);const r=21522^(e<<10|n);for(let t=0;t<=5;t++)this.setFunctionModule(8,t,getBit(r,t));this.setFunctionModule(8,7,getBit(r,6)),this.setFunctionModule(8,8,getBit(r,7)),this.setFunctionModule(7,8,getBit(r,8));for(let t=9;t<15;t++)this.setFunctionModule(14-t,8,getBit(r,t));for(let t=0;t<8;t++)this.setFunctionModule(this.size-1-t,8,getBit(r,t));for(let t=8;t<15;t++)this.setFunctionModule(8,this.size-15+t,getBit(r,t));this.setFunctionModule(8,this.size-8,!0)}drawVersion(){if(this.version<7)return;let t=this.version;for(let e=0;e<12;e++)t=t<<1^7973*(t>>>11);const e=this.version<<12|t;for(let t=0;t<18;t++){const n=getBit(e,t),r=this.size-11+t%3,o=Math.floor(t/3);this.setFunctionModule(r,o,n),this.setFunctionModule(o,r,n)}}drawFinderPattern(t,e){for(let n=-4;n<=4;n++)for(let r=-4;r<=4;r++){const o=Math.max(Math.abs(r),Math.abs(n)),s=t+r,i=e+n;s>=0&&s=0&&i{(t!==a-o||n>=i)&&u.push(e[t])}));return u}drawCodewords(t){if(t.length!==Math.floor(getNumRawDataModules(this.version)/8))throw new RangeError("Invalid argument");let e=0;for(let n=this.size-1;n>=1;n-=2){6===n&&(n=5);for(let r=0;r>>3],7-(7&e)),e++)}}}applyMask(t){if(t<0||t>7)throw new RangeError("Mask value out of range");for(let e=0;e5&&t++):(this.finderPenaltyAddHistory(r,o),n||(t+=40*this.finderPenaltyCountPatterns(o)),n=this.modules[e][s],r=1);t+=40*this.finderPenaltyTerminateAndCount(n,r,o)}for(let e=0;e5&&t++):(this.finderPenaltyAddHistory(r,o),n||(t+=40*this.finderPenaltyCountPatterns(o)),n=this.modules[s][e],r=1);t+=40*this.finderPenaltyTerminateAndCount(n,r,o)}for(let e=0;et+(e?1:0)),e);const n=this.size*this.size;return t+=10*(Math.ceil(Math.abs(20*e-10*n)/n)-1),t}getAlignmentPatternPositions(){if(1===this.version)return[];{const t=Math.floor(this.version/7)+2,e=32===this.version?26:2*Math.ceil((4*this.version+4)/(2*t-2)),n=[6];for(let r=this.size-7;n.length0&&t[2]===e&&t[3]===3*e&&t[4]===e&&t[5]===e;return(n&&t[0]>=4*e&&t[6]>=e?1:0)+(n&&t[6]>=4*e&&t[0]>=e?1:0)}finderPenaltyTerminateAndCount(t,e,n){return t&&(this.finderPenaltyAddHistory(e,n),e=0),e+=this.size,this.finderPenaltyAddHistory(e,n),this.finderPenaltyCountPatterns(n)}finderPenaltyAddHistory(t,e){0===e[0]&&(t+=this.size),e.pop(),e.unshift(t)}}function appendBits(t,e,n){if(e<0||e>31||t>>>e!=0)throw new RangeError("Value out of range");for(let r=e-1;r>=0;r--)n.push(t>>>r&1)}function getBit(t,e){return 0!=(t>>>e&1)}class QrSegment{constructor(t,e,n){if(this.mode=t,this.numChars=e,this.bitData=n,e<0)throw new RangeError("Invalid argument");this.bitData=n.slice()}getData(){return this.bitData.slice()}}const MODE_NUMERIC=[1,10,12,14],MODE_ALPHANUMERIC=[2,9,11,13],MODE_BYTE=[4,8,16,16];function numCharCountBits(t,e){return t[Math.floor((e+7)/17)+1]}function makeBytes(t){const e=[];for(const n of t)appendBits(n,8,e);return new QrSegment(MODE_BYTE,t.length,e)}function makeNumeric(t){if(!isNumeric(t))throw new RangeError("String contains non-numeric characters");const e=[];for(let n=0;n=1<MAX_VERSION)throw new RangeError("Version number out of range");let e=(16*t+128)*t+64;if(t>=2){const n=Math.floor(t/7)+2;e-=(25*n-10)*n-55,t>=7&&(e-=36)}return e}function getNumDataCodewords(t,e){return Math.floor(getNumRawDataModules(t)/8)-ECC_CODEWORDS_PER_BLOCK[e[0]][t]*NUM_ERROR_CORRECTION_BLOCKS[e[0]][t]}function reedSolomonComputeDivisor(t){if(t<1||t>255)throw new RangeError("Degree out of range");const e=[];for(let n=0;n0));for(const r of t){const t=r^n.shift();n.push(0),e.forEach(((e,r)=>n[r]^=reedSolomonMultiply(e,t)))}return n}function reedSolomonMultiply(t,e){if(t>>>8!=0||e>>>8!=0)throw new RangeError("Byte out of range");let n=0;for(let r=7;r>=0;r--)n=n<<1^285*(n>>>7),n^=(e>>>r&1)*t;return n}function encodeSegments(t,e,n=1,r=40,o=-1,s=!0){if(!(MIN_VERSION<=n&&n<=r&&r<=MAX_VERSION)||o<-1||o>7)throw new RangeError("Invalid value");let i,a;for(i=n;;i++){const n=8*getNumDataCodewords(i,e),o=getTotalBits(t,i);if(o<=n){a=o;break}if(i>=r)throw new RangeError("Data too long")}for(const t of[MEDIUM,QUARTILE,HIGH])s&&a<=8*getNumDataCodewords(i,t)&&(e=t);const h=[];for(const e of t){appendBits(e.mode[0],4,h),appendBits(e.numChars,numCharCountBits(e.mode,i),h);for(const t of e.getData())h.push(t)}const l=8*getNumDataCodewords(i,e);appendBits(0,Math.min(4,l-h.length),h),appendBits(0,(8-h.length%8)%8,h);for(let t=236;h.length0));return h.forEach(((t,e)=>u[e>>>3]|=t<<7-(7&e))),new QrCode(i,e,u,o)}function encode(t,e){const{ecc:n="L",boostEcc:r=!1,minVersion:o=1,maxVersion:s=40,maskPattern:i=-1,border:a=1}=e||{},h="string"==typeof t?makeSegments(t):Array.isArray(t)?[makeBytes(t)]:void 0;if(!h)throw new Error("uqr only supports encoding string and binary data, but got: "+typeof t);const l=encodeSegments(h,EccMap[n],o,s,i,r),u=addBorder({version:l.version,maskPattern:l.mask,size:l.size,data:l.modules,types:l.types},a);return e?.invert&&(u.data=u.data.map((t=>t.map((t=>!t))))),e?.onEncoded?.(u),u}function addBorder(t,e=1){if(!e)return t;const{size:n}=t,r=n+2*e;t.size=r,t.data.forEach((t=>{for(let n=0;n!1))),t.data.push(Array.from({length:r},(t=>!1)));const o=QrCodeDataType.Border;t.types.forEach((t=>{for(let n=0;no))),t.types.push(Array.from({length:r},(t=>o)));return t} +return L.Class.extend({ + renderSVG: function(data, options = {}) { + const result = encode(data, options); + const { + pixelSize = 1, + whiteColor = "white", + blackColor = "black" + } = options; + const height = result.size * pixelSize; + const width = result.size * pixelSize; + let svg = ``; + const pathes = []; + for (let row = 0; row < result.size; row++) { + for (let col = 0; col < result.size; col++) { + const x = col * pixelSize; + const y = row * pixelSize; + if (result.data[row][col]) + pathes.push(`M${x},${y}h${pixelSize}v${pixelSize}h-${pixelSize}z`); + } + } + svg += ``; + svg += ``; + svg += ""; + return svg; + }, +}); diff --git a/modules/luci-base/root/usr/libexec/generate_otp.uc b/modules/luci-base/root/usr/libexec/generate_otp.uc new file mode 100644 index 000000000000..aa2bc74673a1 --- /dev/null +++ b/modules/luci-base/root/usr/libexec/generate_otp.uc @@ -0,0 +1,340 @@ +#!/usr/bin/ucode + +// MIT License + +// Copyright (c) 2024 Christian Marangi + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in all +// copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import { cursor } from 'uci'; + +function sToc(s) { + return ord(s); +} + +function cTos(c) { + return chr(c); +} + +const base32_encode_table = map([ + "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", + "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", + "Y", "Z", "2", "3", "4", "5", "6", "7" +], sToc); + +function strToBin(string) +{ + let res = []; + + for (i = 0; i < length(string); i++) + res[i] = ord(string, i); + + return res; +} + +function intToBin(int) +{ + let res = []; + + res[0] = (int >> 24) & 0xff; + res[1] = (int >> 16) & 0xff; + res[2] = (int >> 8) & 0xff; + res[3] = int & 0xff; + + return res; +} + +function binToStr(bin) +{ + return join("", map(bin, cTos)); +} + +function circular_shift(val, shift) +{ + return ((val << shift) | (val >> (32 - shift))) & 0xFFFFFFFF +} + +function encode_base32(string) +{ + const binary_string = strToBin(string); + + let pos = 0; + let pos_in_byte = 7; + + let consumed = 0; + const to_consume = length(string) * 8; + + let out = []; + let out_pos = 0; + let out_pos_in_byte = 4; + + while (true) { + let bit = (binary_string[pos] >> pos_in_byte) & 0x1; + out[out_pos] |= bit << out_pos_in_byte; + consumed++; + + if (consumed == to_consume) + break; + + pos_in_byte--; + if (pos_in_byte == -1) { + pos_in_byte = 7; + pos++; + } + + out_pos_in_byte--; + if (out_pos_in_byte == -1) { + out_pos_in_byte = 4; + out_pos++; + } + } + + for (i = 0; i <= out_pos; i++) + out[i] = base32_encode_table[out[i]]; + + for (i = 0; i < (8 - (out_pos + 1) % 8); i++) + out[(out_pos + 1) + i] = ord("="); + + return binToStr(out); +} + +function calculate_sha1(binary_string) { + let len = length(binary_string); + + // Init primitives + let h0 = 0x67452301; + let h1 = 0xEFCDAB89; + let h2 = 0x98BADCFE; + let h3 = 0x10325476; + let h4 = 0xC3D2E1F0; + + let padded_string = []; + + for (i=0; i < len; i++) + padded_string[i] = binary_string[i]; + + // Add 0x80 = 1 0000000 + padded_string[len++] = 0x80; + + // Pad of required zeros + to_pad = 64 - ((len + 8) % 64); + for (i = 0; i < to_pad; i++) + padded_string[len++] = 0x0; + + // Add length 8 bytes (big endian) + padded_string[len++] = 0x0; + padded_string[len++] = 0x0; + padded_string[len++] = 0x0; + padded_string[len++] = 0x0; + padded_string[len++] = 0x0; + padded_string[len++] = 0x0; + padded_string[len++] = 0x0; + padded_string[len++] = length(binary_string) * 8; + + + for (i = 0; i < len; i+=64) { + let block = []; + + // Convert section to 16 32 bytes block + for (i2 = 0, j = 0; i2 < 16; i2++, j+=4) { + block[i2] = padded_string[i + j] << 24; + block[i2] |= padded_string[i + j + 1] << 16; + block[i2] |= padded_string[i + j + 2] << 8; + block[i2] |= padded_string[i + j + 3]; + } + + // Expand to 80 bytes block + for (j = 16; j < 80; j++) + block[j] = circular_shift(block[j - 3] ^ block[j - 8] ^ block[j - 14] ^ block[j - 16], 1); + + // Init primitives for block + let a = h0; + let b = h1; + let c = h2; + let d = h3; + let e = h4; + + for (j = 0; j < 80; j++) { + let f = 0; + let k = 0; + + // Setup reange constants + if (j < 20) { + f = (b & c) | ((~b) & d); + k = 0x5A827999; + } else if (j < 40) { + f = b ^ c ^ d; + k = 0x6ED9EBA1; + } else if (j < 60) { + f = (b & c) | (b & d) | (c & d); + k = 0x8F1BBCDC; + } else { + f = b ^ c ^ d; + k = 0xCA62C1D6; + } + + let temp = circular_shift(a, 5) + f + e + k + block[j] & 0xFFFFFFFF; + e = d; + d = c; + c = circular_shift(b, 30); + b = a; + a = temp; + } + + // Update hash values + h0 = (h0 + a) & 0xFFFFFFFF; + h1 = (h1 + b) & 0xFFFFFFFF; + h2 = (h2 + c) & 0xFFFFFFFF; + h3 = (h3 + d) & 0xFFFFFFFF; + h4 = (h4 + e) & 0xFFFFFFFF; + } + + let sha1 = []; + + h0_binary = intToBin(h0); + for (i = 0; i < length(h0_binary); i++) + sha1[i] = h0_binary[i]; + + h1_binary = intToBin(h1); + for (i = 0; i < length(h1_binary); i++) + sha1[i+4] = h1_binary[i]; + + h2_binary = intToBin(h2); + for (i = 0; i < length(h2_binary); i++) + sha1[i+8] = h2_binary[i]; + + h3_binary = intToBin(h3); + for (i = 0; i < length(h3_binary); i++) + sha1[i+12] = h3_binary[i]; + + h4_binary = intToBin(h4); + for (i = 0; i < length(h4_binary); i++) + sha1[i+16] = h4_binary[i]; + + return sha1; +} + +function calculate_hmac_sha1(key, message) { + const message_binary = strToBin(message); + let binary_key = strToBin(key); + + if (length(key) > 64) + binary_key = calculate_sha1(binary_key); + + for (i = 0; i < 64 - length(key); i++) + binary_key[length(key)+i] = 0x0; + + let ko = []; + for (i = 0; i < 64; i++) + ko[i] = binary_key[i] ^ 0x36; + + for (i = 0; i < length(message); i++) + ko[64+i] = message_binary[i]; + + const sha1_ko = calculate_sha1(ko); + + ko = []; + + for (i = 0; i < 64; i++) + ko[i] = binary_key[i] ^ 0x5c; + + for (i = 0; i < length(sha1_ko); i++) + ko[64+i] = sha1_ko[i]; + + const hmac = calculate_sha1(ko); + + return hmac; +} + +function calculate_hotp(key, counter) +{ + const secret = encode_base32(key); + const counter_bytes = [ 0x0, 0x0, 0x0, 0x0, + (counter >> 24) & 0xff, + (counter >> 16) & 0xff, + (counter >> 8) & 0xff, + counter & 0xff ]; + + const digest = calculate_hmac_sha1(secret, binToStr(counter_bytes)); + + const offset_bits = digest[19] & 0xf; + + let p = []; + for (i = 0; i < 4; i++) + p[i] = digest[offset_bits+i]; + + const snum = (p[0] << 24 | p[1] << 16 | p[2] << 8 | p[3]) & 0x7fffffff; + const otp = snum % 10 ** 6; + + return otp; +} + +function get_otp(username) +{ + const ctx = cursor(); + + let key = ctx.get('luci', username, 'key'); + if (!key) { + printf("Missing key for user %s\n", username); + exit(1); + } + + let otp_type = ctx.get('luci', username, 'type'); + let counter = ""; + + // Time-based OTP (require synced time with world) + // + // Step is used to device epoch in n step and calculate + // the counter + if (otp_type == "totp") { + let step = ctx.get('luci', username, 'step'); + if (!step) { + printf("Missing step for user %s\n", username); + exit(1); + } + + counter = time()/step; + // Counter-based OTP + // + // OTP is calculated from the counter value. Each different + // counter will generate a different password. + } else if (otp_type == "hotp") { + counter = ctx.get('luci', username, 'counter'); + if (!counter) { + printf("Missing counter for user %s\n", username); + exit(1); + } + } else { + printf("Error Invalid OTP type\n"); + exit(1); + } + + const otp = calculate_hotp(key, counter); + + // With HOTP increment saved counter since we just + // generated a new OTP. + if (otp_type == "hotp") { + ctx.set('luci', username, 'counter', int(counter) + 1); + ctx.commit('luci'); + } + + return otp; +} + +printf("%s", get_otp("root")); diff --git a/modules/luci-base/root/usr/share/rpcd/ucode/luci b/modules/luci-base/root/usr/share/rpcd/ucode/luci index 3c4fea46911f..2535a4535b1e 100644 --- a/modules/luci-base/root/usr/share/rpcd/ucode/luci +++ b/modules/luci-base/root/usr/share/rpcd/ucode/luci @@ -581,6 +581,24 @@ const methods = { return { result: ports }; } + }, + + verifyOTP: { + args: { otp: '' }, + call: function(request) { + const otp = request.args.otp; + + if (!otp) + return { error: 'Invalid OTP' }; + + fd = popen('/usr/libexec/generate_otp.uc'); + if (!fd) + return { error: 'Invalid OTP' }; + + const verify_otp = fd.read('all'); + + return { result: verify_otp == otp }; + } } }; diff --git a/modules/luci-base/ucode/dispatcher.uc b/modules/luci-base/ucode/dispatcher.uc index 8717385be217..dc58f9c5f9cf 100644 --- a/modules/luci-base/ucode/dispatcher.uc +++ b/modules/luci-base/ucode/dispatcher.uc @@ -489,7 +489,7 @@ function syslog(prio, msg) { warn(sprintf("[%s] %s\n", prio, msg)); } -function session_setup(user, pass, path) { +function session_setup(user, pass, otp, path) { let timeout = uci.get('luci', 'sauth', 'sessiontime'); let login = ubus.call("session", "login", { username: user, @@ -497,7 +497,11 @@ function session_setup(user, pass, path) { timeout: timeout ? +timeout : null }); - if (type(login?.ubus_rpc_session) == 'string') { + let verify_otp = ubus.call("luci", "verifyOTP", { + otp: otp + }); + + if (verify_otp?.result && type(login?.ubus_rpc_session) == 'string') { ubus.call("session", "set", { ubus_rpc_session: login.ubus_rpc_session, values: { token: randomid(16) } @@ -911,14 +915,16 @@ dispatch = function(_http, path) { if (!session && resolved.ctx.auth.login) { let user = http.getenv('HTTP_AUTH_USER'); let pass = http.getenv('HTTP_AUTH_PASS'); + let otp = ""; if (user == null && pass == null) { user = http.formvalue('luci_username'); pass = http.formvalue('luci_password'); + otp = http.formvalue('luci_otp'); } if (user != null && pass != null) - session = session_setup(user, pass, resolved.ctx.request_path); + session = session_setup(user, pass, otp, resolved.ctx.request_path); if (!session) { resolved.ctx.path = []; diff --git a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js index f7c134678313..71264fb7982f 100644 --- a/modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js +++ b/modules/luci-mod-system/htdocs/luci-static/resources/view/system/system.js @@ -6,9 +6,12 @@ 'require rpc'; 'require form'; 'require tools.widgets as widgets'; +'require tools.prng as random'; +'require uqr'; var callRcList, callRcInit, callTimezone, - callGetLocaltime, callSetLocaltime, CBILocalTime; + callGetLocaltime, callSetLocaltime, CBILocalTime, + CBIGenerateOTPKey; callRcList = rpc.declare({ object: 'rc', @@ -92,6 +95,28 @@ CBILocalTime = form.DummyValue.extend({ }, }); +CBIGenerateOTPKey = form.DummyValue.extend({ + renderWidget: function(section_id, option_id, cfgvalue) { + return E([], [ + E('input', { + 'type': 'text', + 'value': cfgvalue + }), + E('br'), + E('span', { 'class': 'control-group' }, [ + E('button', { + 'class': 'cbi-button cbi-button-apply', + 'click': ui.createHandlerFn(this, function() { + // TODO generate a random passwordbig enough + return 0; + }), + 'disabled': (this.readonly != null) ? this.readonly : this.map.readonly + }, _('Generate Password')), + ]) + ]); + } +}) + return view.extend({ load: function() { return Promise.all([ @@ -123,6 +148,7 @@ return view.extend({ s.tab('logging', _('Logging')); s.tab('timesync', _('Time Synchronization')); s.tab('language', _('Language and Style')); + s.tab('2factauth', _('2-Factor Auth')); /* * System Properties @@ -307,6 +333,45 @@ return view.extend({ }; } + /* + * 2-Factor Autherntication + */ + o = s.taboption('2factauth', CBIGenerateOTPKey, 'key', _('Key')) + o.uciconfig = 'luci'; + o.ucisection = 'root'; + + o = s.taboption('2factauth', form.ListValue, 'type', _('OTP Type')) + o.uciconfig = 'luci'; + o.ucisection = 'root'; + o.default = 'TOTP'; + o.value('totp', 'TOTP'); + o.value('hotp', 'HOTP'); + + o = s.taboption('2factauth', form.Value, 'counter', _('Counter')) + o.uciconfig = 'luci'; + o.ucisection = 'root'; + o.depends('type', 'hotp'); + + o = s.taboption('2factauth', form.Value, 'step', _('Time Step')) + o.uciconfig = 'luci'; + o.ucisection = 'root'; + o.depends('type', 'totp'); + + o = s.taboption('2factauth', form.DummyValue, '_otp_string', _('OTP String')) + o.cfgvalue = function() { + var label = 'TEST'; + var type = uci.get('luci','root','type'); + var key = uci.get('luci','root','key') + if (type == 'htop') + var option = 'counter=' + uci.get('luci','root','counter') + else + var option = 'step=' + uci.get('luci','root','step') + + var otpauth_str = 'otpauth://' + type + '/' + label + '?secret=' + key + '&' + option; + + return E(uqr.renderSVG(otpauth_str, { pixelSize: 5 })); + }; + return m.render().then(function(mapEl) { poll.add(function() { return callGetLocaltime().then(function(t) { @@ -317,4 +382,4 @@ return view.extend({ return mapEl; }); } -}); +}); \ No newline at end of file diff --git a/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/sysauth.ut b/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/sysauth.ut index 15f3b1435b8a..3d987b6f03ac 100644 --- a/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/sysauth.ut +++ b/themes/luci-theme-bootstrap/ucode/template/themes/bootstrap/sysauth.ut @@ -21,6 +21,12 @@ +
+ +
+ +
+