1# Functions for two-factor enrollment and verification 2 3# list_twofactor_providers() 4# Returns a list of all supported providers, each of which is an array ref 5# containing an ID, name and URL for more info 6sub list_twofactor_providers 7{ 8return ( [ 'totp', 'Google Authenticator', 9 'http://en.wikipedia.org/wiki/Google_Authenticator' ], 10 [ 'authy', 'Authy', 11 'http://www.authy.com/' ] ); 12} 13 14# show_twofactor_apikey_authy(&miniserv) 15# Returns HTML for the form for authy-specific provider inputs 16sub show_twofactor_apikey_authy 17{ 18my ($miniserv) = @_; 19my $rv; 20$rv .= ui_table_row($text{'twofactor_apikey'}, 21 ui_textbox("authy_apikey", $miniserv->{'twofactor_apikey'}, 40)); 22return $rv; 23} 24 25# validate_twofactor_apikey_authy(&in, &miniserv) 26# Validates inputs from show_twofactor_apikey_authy, and stores them. Returns 27# undef if OK, or an error message on failure 28sub validate_twofactor_apikey_authy 29{ 30my ($in, $miniserv) = @_; 31my $key = $in->{'authy_apikey'}; 32my $test = $miniserv->{'twofactor_test'}; 33$key =~ /^\S+$/ || return $text{'twofactor_eapikey'}; 34my $host = $test ? "sandbox-api.authy.com" : "api.authy.com"; 35my $port = $test ? 80 : 443; 36my $page = "/protected/xml/app/details?api_key=".&urlize($key); 37my $ssl = $test ? 0 : 1; 38my ($out, $err); 39&http_download($host, $port, $page, \$out, \$err, undef, $ssl, undef, undef, 40 60, 0, 1); 41if ($err =~ /401/) { 42 return $text{'twofactor_eauthykey'}; 43 } 44elsif ($err) { 45 return &text('twofactor_eauthy', $err); 46 } 47$miniserv->{'twofactor_apikey'} = $key; 48return undef; 49} 50 51# show_twofactor_form_authy(&webmin-user) 52# Returns HTML for a form for enrolling for Authy two-factor 53sub show_twofactor_form_authy 54{ 55my ($user) = @_; 56my $rv; 57$rv .= &ui_table_row($text{'twofactor_email'}, 58 &ui_textbox("email", undef, 40)); 59$rv .= &ui_table_row($text{'twofactor_country'}, 60 &ui_textbox("country", undef, 3)); 61$rv .= &ui_table_row($text{'twofactor_phone'}, 62 &ui_textbox("phone", undef, 20)); 63return $rv; 64} 65 66# parse_twofactor_form_authy(&in, &user) 67# Parses inputs from show_twofactor_form_authy, and returns a hash ref with 68# enrollment details on success, or an error message on failure. 69sub parse_twofactor_form_authy 70{ 71my ($in, $user) = @_; 72$in->{'email'} =~ /^\S+\@\S+$/ || return $text{'twofactor_eemail'}; 73$in->{'country'} =~ s/^\+//; 74$in->{'country'} =~ /^\d{1,3}$/ || return $text{'twofactor_ecountry'}; 75$in->{'phone'} =~ /^[0-9\- ]+$/ || return $text{'twofactor_ephone'}; 76return { 'email' => $in->{'email'}, 77 'country' => $in->{'country'}, 78 'phone' => $in->{'phone'} }; 79} 80 81# enroll_twofactor_authy(&details, &user) 82# Attempts to enroll a user for Authy two-factor. Returns undef on success and 83# sets twofactor_id in &user, or an error message on failure. 84sub enroll_twofactor_authy 85{ 86my ($details, $user) = @_; 87my %miniserv; 88&get_miniserv_config(\%miniserv); 89my $host = $miniserv{'twofactor_test'} ? "sandbox-api.authy.com" 90 : "api.authy.com"; 91my $port = $miniserv{'twofactor_test'} ? 80 : 443; 92my $page = "/protected/xml/users/new?api_key=". 93 &urlize($miniserv{'twofactor_apikey'}); 94my $ssl = $miniserv{'twofactor_test'} ? 0 : 1; 95my $content = "user[email]=".&urlize($details->{'email'})."&". 96 "user[country_code]=".&urlize($details->{'country'})."&". 97 "user[cellphone]=".&urlize($details->{'phone'}); 98my ($out, $err); 99&http_post($host, $port, $page, $content, \$out, \$err, undef, $ssl, undef, 100 undef, 60, 0, 1); 101return $err if ($err); 102if ($out =~ /<id[^>]*>([^<]+)<\/id>/i) { 103 $user->{'twofactor_id'} = $1; 104 $user->{'twofactor_apikey'} = $miniserv{'twofactor_apikey'}; 105 return undef; 106 } 107else { 108 return &text('twofactor_eauthyenroll', 109 "<pre>".&html_escape($out)."</pre>"); 110 } 111} 112 113# validate_twofactor_authy(id, token, apikey) 114# Checks the validity of some token for a user ID 115sub validate_twofactor_authy 116{ 117my ($id, $token, $apikey) = @_; 118$id =~ /^\d+$/ || return $text{'twofactor_eauthyid'}; 119$token =~ /^\d+$/ || return $text{'twofactor_eauthytoken'}; 120my %miniserv; 121&get_miniserv_config(\%miniserv); 122my $host = $miniserv{'twofactor_test'} ? "sandbox-api.authy.com" 123 : "api.authy.com"; 124my $port = $miniserv{'twofactor_test'} ? 80 : 443; 125my $page = "/protected/xml/verify/$token/$id?api_key=".&urlize($apikey). 126 "&force=true"; 127my $ssl = $miniserv{'twofactor_test'} ? 0 : 1; 128my ($out, $err); 129&http_download($host, $port, $page, \$out, \$err, undef, $ssl, undef, undef, 130 60, 0, 1); 131if ($err && $err =~ /401/) { 132 # Token rejected 133 return $text{'twofactor_eauthyotp'}; 134 } 135elsif ($err) { 136 # Some other error 137 return $err; 138 } 139elsif ($out && $out =~ /<success[^>]*>([^<]+)<\/success>/i) { 140 if (lc($1) eq "true") { 141 # Worked! 142 return undef; 143 } 144 elsif ($out =~ /<message[^>]*>([^<]+)<\/message>/i) { 145 # Failed, but with a message 146 return $1; 147 } 148 else { 149 # Failed, not sure why 150 return $out; 151 } 152 } 153else { 154 # Unknown output 155 return $out; 156 } 157} 158 159# validate_twofactor_apikey_totp() 160# Checks that the needed Perl module for TOTP is installed. 161sub validate_twofactor_apikey_totp 162{ 163my ($miniserv, $in) = @_; 164eval "use Authen::OATH"; 165if ($@) { 166 return &text('twofactor_etotpmodule', 'Authen::OATH', 167 "../cpan/download.cgi?source=3&cpan=Authen::OATH&mode=2&". 168 "return=/$module_name/&returndesc=".&urlize($text{'index_return'})) 169 } 170return undef; 171} 172 173# show_twofactor_form_totp(&user) 174# Show form allowing the user to choose a twofactor secret 175sub show_twofactor_form_totp 176{ 177my ($user) = @_; 178my $secret = $user->{'twofactor_id'}; 179$secret = undef if ($secret !~ /^[A-Z0-9=]{16}$/i); 180my $rv; 181$rv .= &ui_table_row($text{'twofactor_secret'}, 182 &ui_opt_textbox("totp_secret", $secret, 20, $text{'twofactor_secret1'}, 183 $text{'twofactor_secret0'})); 184return $rv; 185} 186 187# parse_twofactor_form_totp(&in, &user) 188# Generate or use a secret key for this user 189sub parse_twofactor_form_totp 190{ 191my ($in, $user) = @_; 192if ($in->{'totp_secret_def'}) { 193 $user->{'twofactor_id'} = &encode_base32(&generate_base32_secret()); 194 } 195else { 196 $in{'totp_secret'} =~ /^[A-Z0-9=]{16}$/i || 197 return $text{'twofactor_esecret'}; 198 $user->{'twofactor_id'} = $in{'totp_secret'}; 199 } 200return { }; 201} 202 203# generate_base32_secret([length]) 204# Returns a base-32 encoded secret of by default 10 bytes 205sub generate_base32_secret 206{ 207my ($length) = @_; 208$length ||= 10; 209&seed_random(); 210my $secret = ""; 211while(length($secret) < $length) { 212 $secret .= chr(rand()*256); 213 } 214return $secret; 215} 216 217# enroll_twofactor_totp(&in, &user) 218# Generate a secret for this user, based-32 encoded 219sub enroll_twofactor_totp 220{ 221my ($in, $user) = @_; 222$user->{'twofactor_id'} ||= &encode_base32(&generate_base32_secret()); 223return undef; 224} 225 226# message_twofactor_totp(&user) 227# Returns HTML to display after a user enrolls 228sub message_twofactor_totp 229{ 230my ($user) = @_; 231my $name = &urlize(&get_display_hostname() . " (" . $user->{'name'} . ")"); 232my $url = "https://chart.googleapis.com/chart". 233 "?chs=200x200&chld=M|0&cht=qr&chl=otpauth://totp/". 234 $name."%3Fsecret%3D".$user->{'twofactor_id'}; 235my $rv; 236$rv .= &text('twofactor_qrcode', "<tt>$user->{'twofactor_id'}</tt>")."<p>\n"; 237$rv .= "<img src='$url' border=0><p>\n"; 238return $rv; 239} 240 241# validate_twofactor_totp(id, token, apikey) 242# Checks the validity of some token with google authenticator 243sub validate_twofactor_totp 244{ 245my ($id, $token, $apikey) = @_; 246$id =~ /^[A-Z0-9=]+$/i || return $text{'twofactor_etotpid'}; 247$token =~ /^\d+$/ || return $text{'twofactor_etotptoken'}; 248eval "use Authen::OATH"; 249if ($@) { 250 return &text('twofactor_etotpmodule2', 'Authen::OATH'); 251 } 252my $secret = &decode_base32($id); 253my $oauth = Authen::OATH->new(); 254my $now = time(); 255foreach my $t ($now - 30, $now, $now + 30) { 256 my $expected = $oauth->totp($secret, $t); 257 return undef if ($expected eq $token); 258 } 259return $text{'twofactor_etotpmatch'}; 260} 261 262# get_user_twofactor(username, &miniserv) 263# Returns the twofactor provider, ID and API key for a user 264sub get_user_twofactor 265{ 266my ($user, $miniserv) = @_; 267return () if (!$miniserv->{'twofactorfile'}); 268my $lref = &read_file_lines($miniserv->{'twofactorfile'}, 1); 269foreach my $l (@$lref) { 270 my @two = split(/:/, $l, -1); 271 if ($two[0] eq $user) { 272 return ($two[1], $two[2], $two[3]); 273 } 274 } 275return (); 276} 277 278# save_user_twofactor(username, &miniserv, [provider, id, api-key]) 279# Updates or removes the twofactor provider for a user 280sub save_user_twofactor 281{ 282my ($user, $miniserv, $prov, $id, $key) = @_; 283return 0 if (!$miniserv->{'twofactorfile'}); 284&lock_file($miniserv->{'twofactorfile'}); 285my $lref = &read_file_lines($miniserv->{'twofactorfile'}); 286my $found = 0; 287my $i = 0; 288foreach my $l (@$lref) { 289 my @two = split(/:/, $l, -1); 290 if ($two[0] eq $user) { 291 # Found the line to update or remove 292 if ($prov) { 293 $lref->[$i] = join(":", $user, $prov, $id, $key); 294 } 295 else { 296 splice(@$lref, $i, 1); 297 } 298 $found++; 299 last; 300 } 301 $i++; 302 } 303if (!$found && $prov) { 304 # Need to add the user 305 push(@$lref, join(":", $user, $prov, $id, $key)); 306 } 307&flush_file_lines($miniserv->{'twofactorfile'}); 308&unlock_file($miniserv->{'twofactorfile'}); 309} 310 3111; 312