Use more appropriate URL base64 encoding and colon separators
[blerg.git] / aux / cgi / recovery.cgi
1 #!/usr/bin/perl
2 use CGI::Fast qw/:cgi/;
3 use Digest::SHA qw/hmac_sha256/;
4 use MIME::Base64 qw/encode_base64url/;
5 use Blerg::Database;
6 use strict;
7 use v5.10;
8
9 my $hmac_key;
10 open HMAC_KEY, "$ENV{BLERG_HOME}/etc/hmac_key"
11     or die "Could not open $ENV{BLERG_HOME}/etc/hmac_key";
12 read(HMAC_KEY, $hmac_key, 256);
13 close HMAC_KEY;
14
15 sub print_404 {
16     print header(-type => 'text/html',
17                  -status => '404 Not Found');
18     print <<DOC;
19 <!DOCTYPE html>
20 <h1>404 Not Found</h1>
21 DOC
22 }
23
24 sub print_403 {
25     print header(-type => 'text/html',
26                  -status => '403 Forbidden');
27     print <<DOC;
28 <!DOCTYPE html>
29 <h1>403 Forbidden</h1>
30 Please log in first.
31 DOC
32 }
33
34 sub generate_reset_url {
35     my ($username, $validity) = @_;
36
37     # generate reset data
38     my $expiry = time + $validity;
39     my $counter = Blerg::Database::auth_get_counter($username)
40         or return undef;
41     my $data = "$username:$expiry:$counter";
42         
43     # HMAC-SHA256 it
44     my $hmac = encode_base64url(hmac_sha256($data, $hmac_key));
45
46     return Blerg::Database::BASEURL . "#/recovery/$data:$hmac";
47 }
48
49 sub validate_reset_data {
50     my ($data) = @_;
51     my ($payload, $hmac);
52
53     if ($data =~ /^(.*):([^:]+)$/) {
54         $payload = $1;
55         $hmac = $2;
56     } else {
57         return undef;
58     }
59
60     my $computed_hmac = encode_base64url(hmac_sha256($payload, $hmac_key));
61     if ($hmac ne $computed_hmac) {
62         return undef;
63     }
64
65     my ($username, $expiry, $counter) = split(':', $payload);
66     if (time > $expiry
67         || $counter != Blerg::Database::auth_get_counter($username)) {
68         return undef;
69     }
70
71     return $username;
72 }
73
74 REQUEST:
75 while (my $q = new CGI::Fast) {
76     my (undef, $cmd, $args) = split '/', $ENV{PATH_INFO}, 3;
77
78     given ($cmd) {
79         when ('new') {
80             # determine that authentication is valid.
81             my $auth = $q->cookie('auth');
82             if (!defined $auth) {
83                 print_403;
84                 next REQUEST;
85             }
86             my ($username, $token) = split('/', $auth);
87             if (!Blerg::Database::auth_check_token($username, $token)) {
88                 print_403;
89                 next REQUEST;
90             }
91
92             my $validity = 365 * 86400;     # One year
93             print header(-type => 'text/plain');
94             print generate_reset_url($username, $validity);
95         }
96         when ('mail') {
97             # check that the user has a validated mail address
98             # generate reset message
99             # send mail
100         }
101         when ('validate') {
102             print header(-type => 'application/json');
103
104             my $username = validate_reset_data($q->param('data'));
105
106             if (!defined $username) {
107                 say '{"status": "failure"}';
108                 next REQUEST;
109             }
110
111             my $password = $q->param('password');
112             if (Blerg::Database::auth_set_password($username, $password)) {
113                 say '{"status": "success"}';
114             } else {
115                 say '{"status": "failure"}';
116             }
117         }
118         default {
119             print_404;
120         }
121     }
122 }