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