On Sun, Mar 19, 2017 at 01:19:05PM -0400, Ineiev wrote: > > I'll need more hints to address points 3. (what more validation > and error checking could be used in the Perl script), 4. (I don't know > how we could usefully sanitize GPG input), 5. (how the 'open' > command should escape parameters before executing them as shell > commands).
This is to address points 3. (more checking is added to the Perl script) and 5. (a check is added to make sure that key_id is a hexadecimal number). I still doubt we really need to run any checks on the provided GPG key: typical gpg usage includes importing keys from untrusted key servers.
From 3546b4b74719f74f8af7cafc10e23439553a001a Mon Sep 17 00:00:00 2001 From: Ineiev <ine...@gnu.org> Date: Fri, 10 Feb 2017 15:10:55 +0300 Subject: [PATCH] Encrypt message with GPG key when available. --- frontend/perl/encrypt-to-user/index.pl | 157 ++++++++++++++++++++++++++++++++ frontend/php/account/lostpw-confirm.php | 56 +++++++++++- frontend/php/my/admin/index.php | 18 +++- 3 files changed, 229 insertions(+), 2 deletions(-) create mode 100644 frontend/perl/encrypt-to-user/index.pl diff --git a/frontend/perl/encrypt-to-user/index.pl b/frontend/perl/encrypt-to-user/index.pl new file mode 100644 index 0000000..8729b5b --- /dev/null +++ b/frontend/perl/encrypt-to-user/index.pl @@ -0,0 +1,157 @@ +#! /usr/bin/perl +# Encrypt a message to specified savane user. +# +# Copyright 2017 (c) Ineiev <ineiev--gnu.org> +# +# This file is part of Savane. +# +# Savane is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License as +# published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# Savane is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see <http://www.gnu.org/licenses/>. + +use strict; +use DBI; +use File::Temp qw(tempdir tempfile); +use Getopt::Long; +my $getopt; +my $help; +my $user; +my $sys_dbname; +my $sys_dbhost; +my $sys_dbuser; +my $sys_dbparams; +my $sys_dbpasswd; +my $exit_code = 0; + +eval { + $getopt = GetOptions("help" => \$help, + "user=s" => \$user, + "dbname=s" => \$sys_dbname, + "dbhost:s" => \$sys_dbhost, + "dbparams:s" => \$sys_dbparams); +}; + +sub print_help { + print STDERR <<EOF; +Usage: $0 [OPTIONS] + +Encrypt a message to user's registered GPG key. + + -h, --help Display this help and exit + --user Savannah user to encrypt to + --dbname Savannah database name + --dbhost Savannah database host + --dbparams Savannah database parameters + +Database user and password are passed in the first two lines of input. + +EOF +} + +if($help) { + print_help(); + exit(0); +} + +$sys_dbuser = <> or die "No database user is supplied."; +$sys_dbpasswd = <> or die "No database password is supplied."; + +$sys_dbuser =~ s/\n$//; +$sys_dbpasswd =~ s/\n$//; + +our $dbd = DBI->connect('DBI:mysql:database='.$sys_dbname + .':host='.$sys_dbhost + .$sys_dbparams, + $sys_dbuser, $sys_dbpasswd, + { RaiseError => 1, AutoCommit => 1}); + +## Encrypt to user GPG key if available +# arg1: user id +# arg2: message +# return encrypted message when encryption succeeded, +# empty string encryption failed. +# Exit codes: +# 0 when encryption succeeded, +# 1 when it failed, +# 2 when no suitable key was found, +# 3 when key selection error occurred, +# 4 when creating temporary files failed, +# 5 when extracted key_id is invalid. +sub UserEncrypt { + my ($user, $message) = @_; + my $key = $dbd->selectrow_array("SELECT gpg_key FROM user WHERE user_id=".$user); + + $exit_code = 3; + return "" unless $key ne ""; + + $exit_code = 4; + + my ($mh, $mname) = tempfile(UNLINK => 1); + return "" if $mname eq ""; + + my $temp_dir = tempdir(CLEANUP => 1); + return "" if $temp_dir eq ""; + + my $input; + my $key_id = ""; + my $msg = ""; + + print $mh $message; + + $exit_code = 2; + open($input, '|-', 'gpg --homedir='.$temp_dir.' -q --import'); + print $input $key; + close($input) or return ""; + +# Get the first ID of a public key with encryption capability. + open($input, '-|', 'gpg --homedir='.$temp_dir. + ' --list-keys --with-colons 2> /dev/null'); + while(<$input>) + { + if(!/^pub/) + { + next; + } + my @fields = split /:/; + if(@fields[11] !~ /[eE]/) + { + next; + } + $key_id = @fields[4]; + last unless $key_id eq ""; + } + close($input) or return ""; + return "" unless $key_id ne ""; + $exit_code = 5; + return "" unless $key_id =~ /^[0-9A-F]*$/; + $exit_code = 1; + open($input, '-|', 'gpg --homedir='.$temp_dir. + ' --trust-model always --batch -a --encrypt -r ' + .$key_id." -o - ".$mname); + while(<$input>) + { + $msg = $msg.$_; + } + close $input and $exit_code = 0; + return $msg; +} + +my $msg = ""; + +while(<>) + { + $msg = $msg.$_; + } + +print UserEncrypt($user, $msg); + +exit $exit_code; diff --git a/frontend/php/account/lostpw-confirm.php b/frontend/php/account/lostpw-confirm.php index a4dc5cd..a04741b 100644 --- a/frontend/php/account/lostpw-confirm.php +++ b/frontend/php/account/lostpw-confirm.php @@ -4,6 +4,7 @@ # Copyright 1999-2000 (c) The SourceForge Crew # Copyright 2004-2005 (c) Mathieu Roy <yeupou--gnu.org> # Joxean Koret <joxeankoret--yahoo.es> +# Copyright 2017 (c) Ineiev <ineiev--gnu.org> # # This file is part of Savane. # @@ -24,6 +25,7 @@ require_once('../include/init.php'); require_once('../include/sane.php'); require_once('../include/session.php'); require_once('../include/sendmail.php'); +require_once('../include/database.php'); register_globals_off(); @@ -140,6 +142,47 @@ $message_for_admin = . gmdate('D, d M Y H:i:s \G\M\T') . "\n"; +$encrypted_message = ""; +$gpg_error = ""; +if(user_get_preference("email_encrypted", $row_user['user_id'])) + { + $cmd = 'perl ../../perl/encrypt-to-user/index.pl ' + .'--user="'.$row_user['user_id'].'" ' + .'--dbname="'.$sys_dbname.'" ' + .'--dbhost="'.$sys_dbhost.'"'; + + $d_spec = array( + 0 => array("pipe", "r"), 1 => array("pipe", "w"), + 2 => array("file", "/dev/null", "a")); + + $gpg_proc = proc_open($cmd, $d_spec, $pipes, NULL, $_ENV); + fwrite($pipes[0], $sys_dbuser."\n"); + fwrite($pipes[0], $sys_dbpasswd."\n"); + fwrite($pipes[0], $message); + fclose($pipes[0]); + $encrypted_message = stream_get_contents($pipes[1]); + fclose($pipes[1]); + $gpg_result = proc_close($gpg_proc); + + if($gpg_result != 0 or $encrypted_message === FALSE or $encrypted_message === "") + { + $encrypted_message = ""; + if($gpg_result == 1) + $gpg_error = _("Encryption failed."); + else if($gpg_result == 2) + $gpg_error = _("No key for encryption found."); + else if($gpg_result == 3) + $gpg_error = _("Can't extract user_id from database."); + else if($gpg_result == 4) + $gpg_error = _("Can't create temporary files."); + else if($gpg_result == 5) + $gpg_error = _("Extracted GPG key ID is invalid."); + } + } + +if($encrypted_message != "") + $message = $encrypted_message; + sendmail_mail($GLOBALS['sys_mail_replyto']."@".$GLOBALS['sys_mail_domain'], $row_user['email'], $GLOBALS['sys_default_domain']." Verification", @@ -159,6 +202,17 @@ $HTML->header(array('title'=>_("Lost Password Confirmation"))); print '<p>'._("An email has been sent to the address you have on file.").'</p>'; print '<p>'._("Follow the instructions in the email to change your account password.").'</p>'; -; + +if($encrypted_message === "") + { + if(user_get_preference("email_encrypted", $row_user['user_id'])) + print '<p><strong>'.$gpg_error.'<strong></p>'; + print '<blockquote><p>'._("Note that the message was sent unencrypted. +In order to use encryption, register an encryption-capable GPG key +and set the <b>Encrypt emails when resetting password</b> checkbox +in your account settings.").'</p></blockquote>'; + } +else + print '<p>'._("Note that it was encrypted with your registered GPG key.").'</p>'; $HTML->footer(array()); diff --git a/frontend/php/my/admin/index.php b/frontend/php/my/admin/index.php index 3b19a28..cbf5e80 100644 --- a/frontend/php/my/admin/index.php +++ b/frontend/php/my/admin/index.php @@ -3,6 +3,7 @@ # Copyright 1999-2000 (c) The SourceForge Crew # Copyright 2002-2006 (c) Mathieu Roy <yeupou--gnu.org> # Copyright (C) 2007 Sylvain Beucler +# Copyright (C) 2017 Ineiev <ineiev--gnu.org> # # This file is part of Savane. # @@ -30,7 +31,7 @@ extract(sane_import('post', 'form_keep_only_one_session', 'form_timezone', 'user_theme', 'theme_rotate_jump', 'form_reverse_comments_order', 'form_stone_age_menu', 'form_nonfixed_feedback', - 'form_use_bookmarks', 'form_email_hide', + 'form_use_bookmarks', 'form_email_hide', 'form_email_encrypted' ))); if ($update and $user_theme != "random" and $user_theme != "rotate") @@ -79,6 +80,12 @@ if ($update) else { user_unset_preference("use_bookmarks"); } + # Encryption preferences + if ($form_email_encrypted == "1") + { user_set_preference("email_encrypted", 1); } + else + { user_unset_preference("email_encrypted"); } + # Relative position feedback if ($form_nonfixed_feedback == "1") { user_set_preference("nonfixed_feedback", 1); } @@ -287,6 +294,15 @@ print '<input type="checkbox" name="form_email_hide" value="1" '.($row_user['ema print '<p class="smaller">'._("When checked, the only way for users to get in touch with you would be to use the form available to logged-in users. It is generally a bad idea to choose this option, especially if you are a project administrator.").'</p>'; +print '<input type="checkbox" name="form_email_encrypted" value="1" ' +.(user_get_preference("email_encrypted") ? 'checked="checked"':'').' /> ' +._("Encrypt emails when resetting password"); + +print '<p class="smaller">' +._("When checked, Savannah will encrypt email messages +with your registered public GPG key when resetting password is requested. +If no suitable key is available, the messages still go unencrypted.") +.'</p>'; print $HTML->box_bottom(); print "<br />\n"; -- 1.9.1
signature.asc
Description: Digital signature