Diff -- /var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/twofa.cgi
Diff

/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com/twofa.cgi

added on local at 2026-07-01 12:35:16

Added
+227
lines
Removed
-0
lines
Context
0
unchanged
Blobs
from
to 02e8be5a05c1
Restore this content →
Unified diff
Naive LCS-based line diff. Additions in emerald, deletions in rose. Both sides decompressed live from BLOB_STORE.
1#!/usr/bin/perl
2
3##################################################################################################################################
4# ______ ____ ____ ______ ______ ____ ___ __ ___ ______ _ __ ____ ____ __ __ _ __ _____ ____
5# / ____// __ \ / __ \ / ____/ / ____// __ \ / | / |/ // ____/| | / // __ \ / __ \ / //_/ | | / /|__ / / __ \
6# / / / / / // / / // __/ / /_ / /_/ // /| | / /|_/ // __/ | | /| / // / / // /_/ // ,< | | / / /_ < / / / /
7# / /___ / /_/ // /_/ // /___ / __/ / _, _// ___ | / / / // /___ | |/ |/ // /_/ // _, _// /| | | |/ / ___/ /_ / /_/ /
8# \____/ \____//_____//_____/ /_/ /_/ |_|/_/ |_|/_/ /_//_____/ |__/|__/ \____//_/ |_|/_/ |_| |___/ /____/(_)\____/
9#
10# - Written by: Shawn Pickering
11# - shawn@shawnpickering.com
12##################################################################################################################################
13use strict;
14use lib '/var/www/vhosts/3dshawn.com/ptmatrix.3dshawn.com';
15
16
17#############################################################
18## Variables
19#############################################################
20
21
22
23############################################################
24# Modules
25############################################################
26#Load the CGI module
27use CGI;
28my $query = new CGI;
29my $form = $query->Vars;
30
31#Load the login module
32use MODS::Login;
33my $login = new MODS::Login;
34
35#Load Urls
36use MODS::PTMatrix::Urls;
37my $getlist = new MODS::PTMatrix::Urls;
38my $url = $getlist->urls();
39
40#Load the template module
41use MODS::Template;
42my $tfile = new MODS::Template;
43
44#Page Wrapper Module
45use MODS::PTMatrix::Wrapper;
46my $load = new MODS::PTMatrix::Wrapper;
47
48#Load the database connect module
49use MODS::DBConnect;
50my $db = new MODS::DBConnect;
51
52#Load the configuration file
53use MODS::PTMatrix::Config;
54my $config = new MODS::PTMatrix::Config;
55my $database_name = $config->settings('database_name');
56
57#Shared PM helpers
58use MODS::PTMatrix::PM;
59
60#TOTP helpers (secret gen, code verify, recovery codes)
61use MODS::PTMatrix::TOTP;
62
63#SHA-256 for recovery-code hashing
64use Digest::SHA qw(sha256_hex);
65
66
67
68#############################################################
69## Get form data
70#############################################################
71#Escapes bad characters
72foreach my $line(keys %$form){
73 $form->{$line} =~ s/[^!-~\s]//g; #Remove non ascii chars
74 $form->{$line} = quotemeta("$form->{$line}");
75}
76
77
78
79############################################################
80# Program Controls
81############################################################
82$|=1;
83my $userinfo = $login->login_verify();
84print qq~Content-type:text/html; Cache-Control:no-cache;\n\n~;
85if(!$userinfo){print qq~<script>window.location="$url->{login}";</script>~;}
86else{&twofa_handler;}
87
88
89############################################################
90# Subroutines
91############################################################
92sub twofa_handler{
93 my $uid = MODS::PTMatrix::PM::get_user_id($userinfo);
94 my $email = $userinfo->{email} || '';
95 my $brand = $config->settings('brand_name') || 'PTMatrix';
96
97 my $dbh = $db->db_connect();
98 my $flash = '';
99 my $err = '';
100 my $show_codes = [];
101
102 #Read the user's current 2FA state - enabled? secret already set?
103 my $sql = qq~SELECT two_factor_enabled, two_factor_secret_enc FROM `${database_name}`.users WHERE id='$uid' LIMIT 1~;
104 my $userrow = $db->db_readwrite($dbh, $sql, $ENV{SCRIPT_NAME}, __LINE__);
105 my $enabled = $userrow && $userrow->{two_factor_enabled} ? 1 : 0;
106 my $secret = $userrow && $userrow->{two_factor_secret_enc} ? $userrow->{two_factor_secret_enc} : '';
107
108 my $act = $form->{act} || '';
109 my $is_post = (($ENV{REQUEST_METHOD}||'') eq 'POST') ? 1 : 0;
110
111 #---- act=enroll - generate a pending secret, jump into setup view -----
112 if($act eq 'enroll' && $is_post){
113 my $sec = MODS::PTMatrix::TOTP::gen_secret();
114 my $sec_q = MODS::PTMatrix::PM::sql_quote($sec);
115 my $sql_u = qq~UPDATE `${database_name}`.users SET two_factor_secret_enc='$sec_q', two_factor_enabled=0 WHERE id='$uid'~;
116 $db->db_readwrite($dbh, $sql_u, $ENV{SCRIPT_NAME}, __LINE__);
117 $secret = $sec;
118 $act = 'setup';
119 }
120
121 #---- act=verify - confirm 6-digit code, enable 2FA, mint recovery codes ----
122 if($act eq 'verify' && $is_post){
123 my $code = $form->{code} || '';
124 $code =~ s/\D//g;
125 if($secret && MODS::PTMatrix::TOTP::verify($secret, $code)){
126 $db->db_readwrite($dbh, qq~UPDATE `${database_name}`.users SET two_factor_enabled=1 WHERE id='$uid'~, $ENV{SCRIPT_NAME}, __LINE__);
127
128 #Wipe any old recovery codes and mint a fresh set of 10
129 $db->db_readwrite($dbh, qq~DELETE FROM `${database_name}`.user_2fa_recovery_codes WHERE user_id='$uid'~, $ENV{SCRIPT_NAME}, __LINE__);
130 my @codes = MODS::PTMatrix::TOTP::gen_recovery_codes(10);
131 foreach my $c(@codes){
132 my $hsh = sha256_hex($c);
133 $db->db_readwrite($dbh, qq~INSERT INTO `${database_name}`.user_2fa_recovery_codes (user_id, code_hash) VALUES ('$uid', '$hsh')~, $ENV{SCRIPT_NAME}, __LINE__);
134 }
135 $show_codes = \@codes;
136 $flash = 'Two-factor authentication enabled. Save these recovery codes &mdash; they are shown ONCE.';
137 $enabled = 1;
138 }else{
139 $err = 'That code did not match. Double-check the time on your device, then try again.';
140 $act = 'setup';
141 }
142 }
143
144 #---- act=disable - require current code, then turn 2FA off + clear codes ----
145 if($act eq 'disable' && $is_post){
146 my $code = $form->{code} || ''; $code =~ s/\D//g;
147 if($enabled && $secret && MODS::PTMatrix::TOTP::verify($secret, $code)){
148 $db->db_readwrite($dbh, qq~UPDATE `${database_name}`.users SET two_factor_enabled=0, two_factor_secret_enc=NULL WHERE id='$uid'~, $ENV{SCRIPT_NAME}, __LINE__);
149 $db->db_readwrite($dbh, qq~DELETE FROM `${database_name}`.user_2fa_recovery_codes WHERE user_id='$uid'~, $ENV{SCRIPT_NAME}, __LINE__);
150 $flash = 'Two-factor turned off.';
151 $enabled = 0; $secret = '';
152 }else{
153 $err = 'Provide a current 6-digit code to disable 2FA.';
154 }
155 }
156
157 #---- act=regen_codes - rotate just the recovery code set ----
158 if($act eq 'regen_codes' && $is_post){
159 if($enabled){
160 $db->db_readwrite($dbh, qq~DELETE FROM `${database_name}`.user_2fa_recovery_codes WHERE user_id='$uid'~, $ENV{SCRIPT_NAME}, __LINE__);
161 my @codes = MODS::PTMatrix::TOTP::gen_recovery_codes(10);
162 foreach my $c(@codes){
163 my $hsh = sha256_hex($c);
164 $db->db_readwrite($dbh, qq~INSERT INTO `${database_name}`.user_2fa_recovery_codes (user_id, code_hash) VALUES ('$uid', '$hsh')~, $ENV{SCRIPT_NAME}, __LINE__);
165 }
166 $show_codes = \@codes;
167 $flash = 'New recovery codes. Save them now &mdash; the old ones are dead.';
168 }
169 }
170
171 #Count remaining (unused) recovery codes for the panel
172 my $codes_remaining = 0;
173 if($enabled){
174 my $r = $db->db_readwrite($dbh, qq~SELECT COUNT(*) AS n FROM `${database_name}`.user_2fa_recovery_codes WHERE user_id='$uid' AND used_at IS NULL~, $ENV{SCRIPT_NAME}, __LINE__);
175 $codes_remaining = $r ? ($r->{n}||0) : 0;
176 }
177
178 $db->db_disconnect($dbh);
179
180 #Build the QR code URL for the setup view
181 my $otpauth = $secret ? MODS::PTMatrix::TOTP::otpauth_url($brand, $email, $secret) : '';
182 my $qr_url = $otpauth ? 'https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=' . &urlenc($otpauth) : '';
183
184 #Create all template variables
185 my $tvars;
186 $tvars->{flash} = $flash;
187 $tvars->{err} = $err;
188 $tvars->{has_flash} = length($flash) ? 1 : 0;
189 $tvars->{has_err} = length($err) ? 1 : 0;
190 $tvars->{is_enabled} = $enabled;
191 $tvars->{has_secret} = length($secret) ? 1 : 0;
192 $tvars->{is_setup} = ($act eq 'setup' || ($secret && !$enabled)) ? 1 : 0;
193 $tvars->{secret} = $secret;
194 $tvars->{secret_grouped} = &group_secret($secret);
195 $tvars->{otpauth} = $otpauth;
196 $tvars->{qr_url} = $qr_url;
197 $tvars->{show_codes} = scalar(@$show_codes) ? 1 : 0;
198 $tvars->{codes} = [ map { { code => $_ } } @$show_codes ];
199 $tvars->{codes_remaining} = $codes_remaining;
200
201 my $body = join('', $tfile->template('tf_twofa.html', $tvars, $userinfo));
202
203 $load->render({
204 userinfo => $userinfo,
205 page_key => 'profile',
206 title => 'Two-factor authentication',
207 body => $body,
208 });
209}
210
211#Percent-encode arbitrary input - used to ship otpauth:// into the QR API
212sub urlenc{
213 my $s = shift // '';
214 $s =~ s/([^A-Za-z0-9_.\-~])/sprintf("%%%02X", ord $1)/ge;
215 return $s;
216}
217
218#Group a base32 secret in 4-char chunks for the "type me manually" display
219sub group_secret{
220 my $s = shift // '';
221 return '' unless length $s;
222 my @parts;
223 for(my $i = 0; $i < length $s; $i += 4){
224 push @parts, substr($s, $i, 4);
225 }
226 return join(' ', @parts);
227}
Keyboard: j next diff k previous diff g top G bottom r restore c compile-check ? help