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

Attachment: signature.asc
Description: Digital signature

Reply via email to