/[drupal]/contributions/modules/gnupg/gnupg.module
ViewVC logotype

Contents of /contributions/modules/gnupg/gnupg.module

Parent Directory Parent Directory | Revision Log Revision Log | View Revision Graph Revision Graph


Revision 1.7 - (show annotations) (download) (as text)
Wed Feb 18 19:45:13 2009 UTC (9 months, 1 week ago) by arto
Branch: MAIN
CVS Tags: HEAD
Changes since 1.6: +338 -124 lines
File MIME type: text/x-php
Imported the latest development version of the GnuPG module.
1 <?php
2 // $Id$
3
4 //////////////////////////////////////////////////////////////////////////////
5 // Module settings
6
7 define('GNUPG_PATH', drupal_get_path('module', 'gnupg'));
8 define('GNUPG_HOMEDIR', variable_get('gnupg_homedir', GNUPG_PATH . '/.gnupg'));
9 define('GNUPG_KEYID', variable_get('gnupg_keyid', ''));
10
11 //////////////////////////////////////////////////////////////////////////////
12 // Core API hooks
13
14 /**
15 * Implementation of hook_perm().
16 */
17 function gnupg_perm() {
18 return array('administer GnuPG configuration');
19 }
20
21 /**
22 * Implementation of hook_menu().
23 */
24 function gnupg_menu() {
25 return array(
26 // Administer >> Site configuration >> Cryptography
27 'admin/settings/gnupg' => array(
28 'title' => 'Cryptography',
29 'description' => 'Settings for GNU Privacy Guard cryptography.',
30 'access arguments' => array('administer GnuPG configuration'),
31 'page callback' => 'drupal_get_form',
32 'page arguments' => array('gnupg_admin_settings'),
33 'file' => 'gnupg.admin.inc',
34 ),
35 // Administer >> User management >> Public keys
36 'admin/user/gnupg' => array(
37 'title' => 'Public keys',
38 'description' => 'Manage the OpenPGP public keys for your users.',
39 'access arguments' => array('administer users'),
40 'page callback' => 'gnupg_admin_users',
41 'file' => 'gnupg.admin.inc',
42 ),
43 // User profiles >> Public key
44 'user/%user/public-key' => array(
45 'title' => 'Public key',
46 'type' => MENU_LOCAL_TASK,
47 'access callback' => 'gnupg_user_page_access',
48 'access arguments' => array(1),
49 'page callback' => 'gnupg_user_page',
50 'page arguments' => array(1),
51 'weight' => 3,
52 ),
53 );
54 }
55
56 /**
57 * Implementation of hook_cron().
58 */
59 function gnupg_cron() {
60 $keys = db_query("SELECT uri, key_id AS id, key_data AS data FROM {gnupg_keys} WHERE key_id IS NULL ORDER BY timestamp ASC");
61 while ($key = db_fetch_object($keys)) {
62 if (($key->id = gnupg_import_key($key->data))) {
63 db_query("UPDATE {gnupg_keys} SET key_id = '%s' WHERE uri = '%s'", $key->id, $key->uri);
64 }
65 else {
66 watchdog('gnupg', 'Failed to import public key for !link into pubring.gpg.', array('!link' => l($key->uri, $key->uri)), WATCHDOG_ERROR); // TODO: improve this error message.
67 }
68 }
69 }
70
71 //////////////////////////////////////////////////////////////////////////////
72 // User API hooks
73
74 /**
75 * Implementation of hook_user().
76 */
77 function gnupg_user($op, &$edit, &$account, $category = NULL) {
78 switch ($op) {
79 case 'categories':
80 return array(array('name' => 'gnupg', 'title' => t('Cryptography'), 'weight' => 10));
81
82 case 'load':
83 $edit['gnupg_key_id'] = $account->gnupg_key_id = gnupg_get_user_key_id($account->uid);
84 break;
85
86 case 'form':
87 switch ($category) {
88 case 'gnupg':
89 $form['gnupg'] = array('#type' => 'fieldset', '#title' => t('OpenPGP public key'), '#weight' => -10);
90 $form['gnupg']['gnupg_key'] = array(
91 '#type' => 'textarea',
92 '#title' => t('Public key'),
93 '#default_value' => isset($edit['gnupg_key']) ? $edit['gnupg_key'] : gnupg_get_user_key_data($account->uid),
94 '#description' => t('Your public key can be used to, for example, securely e-mail you.'),
95 '#rows' => 12,
96 );
97
98 $form['gnupg']['gnupg_display_key'] = array(
99 '#type' => 'checkbox',
100 '#title' => t('Display my public key on my user profile'),
101 '#default_value' => isset($edit['gnupg_display_key']) ? $edit['gnupg_display_key'] : 1,
102 '#description' => t(''), // TODO
103 );
104
105 if (variable_get('gnupg_mail_encrypt', '') != '') {
106 $form['gnupg']['gnupg_encrypt_mail'] = array(
107 '#type' => 'checkbox',
108 '#title' => t('Encrypt e-mail sent to me (when possible)'),
109 '#default_value' => isset($edit['gnupg_encrypt_mail']) ? $edit['gnupg_encrypt_mail'] : 1,
110 '#description' => t(''), // TODO
111 );
112 if (variable_get('gnupg_mail_encrypt', '') == 'always') {
113 $form['gnupg']['gnupg_encrypt_mail']['#disabled'] = 'disabled';
114 $form['gnupg']['gnupg_encrypt_mail']['#default_value'] = 1;
115 }
116 }
117 return $form;
118 }
119 break;
120
121 case 'validate':
122 if (!empty($edit['gnupg_key'])) {
123 if (strpos($edit['gnupg_key'], '-----BEGIN PGP PUBLIC KEY BLOCK-----') === FALSE ||
124 strpos($edit['gnupg_key'], '-----END PGP PUBLIC KEY BLOCK-----') === FALSE) {
125 form_set_error('gnupg_key', t('Invalid OpenPGP public key. You must paste in an ASCII-armored key block that begins with a <tt>BEGIN PGP PUBLIC KEY BLOCK</tt> marker and ends with a <tt>END PGP PUBLIC KEY BLOCK</tt> marker. Provided you have the GNU Privacy Guard installed on your computer, you can obtain this key by executing the terminal command <tt>gpg --export --armor your.email@example.com</tt>. For other OpenPGP-compatible software such as the commercial PGP suite, please refer to the user documentation provided by the vendor.'));
126 }
127 }
128 break;
129
130 case 'insert':
131 case 'update':
132 gnupg_del_user_key($account->uid);
133 if (!empty($edit['gnupg_key']) && ($key_data = trim($edit['gnupg_key']))) {
134 gnupg_set_user_key($account->uid, $key_data);
135 }
136 $edit['gnupg_key'] = NULL;
137 break;
138
139 case 'delete':
140 gnupg_del_user_key($account->uid);
141 break;
142 }
143 }
144
145 //////////////////////////////////////////////////////////////////////////////
146 // Mail API hooks
147
148 /**
149 * Implementation of hook_mail_alter().
150 */
151 function gnupg_mail_alter(&$message) {
152 if (($encrypt = variable_get('gnupg_mail_encrypt', '')) != '') {
153 if (is_object($account = $message['params']['recipient'])) {
154
155 if ($encrypt == 'always' || !empty($account->gnupg_encrypt_mail)) {
156 if (($key_id = gnupg_get_user_key_id($account->uid))) {
157
158 $options = ($comment = variable_get('gnupg_mail_comment', '')) ? array('comment' => $comment) : array();
159 if (($ciphertext = gnupg_encrypt(implode("\n\n", $message['body']), $key_id, $options))) {
160 $message['body'] = $ciphertext;
161
162 if (($header = variable_get('gnupg_mail_header', ''))) {
163 $message['body'] = $header . "\n\n" . $message['body'];
164 }
165
166 if (($footer = variable_get('gnupg_mail_footer', ''))) {
167 $message['body'] .= "\n" . $footer;
168 }
169 }
170 }
171 }
172 }
173 }
174 }
175
176 //////////////////////////////////////////////////////////////////////////////
177 // GnuPG API implementation
178
179 function gnupg_get_user_key($uid) {
180 return ($key = db_fetch_object(db_query("SELECT key_id AS id, key_data AS data FROM {gnupg_keys} WHERE uri = '%s'", 'user/' . $uid))) ? $key : NULL;
181 }
182
183 function gnupg_get_user_key_id($uid) {
184 return ($key = gnupg_get_user_key($uid)) ? $key->id : NULL;
185 }
186
187 function gnupg_get_user_key_data($uid) {
188 return ($key = gnupg_get_user_key($uid)) ? $key->data : NULL;
189 }
190
191 function gnupg_set_user_key($uid, $key_data, $key_id = NULL) {
192 $key_id = $key_id ? $key_id : gnupg_import_key($key_data);
193 if (!$key_id) {
194 db_query("INSERT INTO {gnupg_keys} (uri, timestamp, key_id, key_data) VALUES ('%s', %d, NULL, '%s')", 'user/' . $uid, time(), $key_data);
195 }
196 else {
197 db_query("INSERT INTO {gnupg_keys} (uri, timestamp, key_id, key_data) VALUES ('%s', %d, '%s', '%s')", 'user/' . $uid, time(), $key_id, $key_data);
198 }
199 }
200
201 function gnupg_del_user_key($uid) {
202 if (($key_id = gnupg_get_user_key_id($uid))) {
203 gnupg_delete_key($key_id); // also delete key from pubring.gpg
204 }
205 db_query("DELETE FROM {gnupg_keys} WHERE uri = '%s'", 'user/' . $uid);
206 }
207
208 /**
209 * Returns TRUE if GnuPG is installed and the GnuPG module has been
210 * correctly configured.
211 */
212 function gnupg_is_available() {
213 return is_dir(GNUPG_HOMEDIR) && is_executable(variable_get('gnupg_exec', gnupg_guess_binpath()));
214 }
215
216 /**
217 * Returns the GnuPG version number.
218 */
219 function gnupg_version() {
220 if (($output = gnupg_exec(array('version' => TRUE))) && !empty($output)) {
221 if (preg_match('!^gpg \(GnuPG\) (.*)$!', $output[0], $matches)) {
222 return $matches[1];
223 }
224 }
225 }
226
227 /**
228 * Returns the record types used in GnuPG's key listings.
229 */
230 function gnupg_get_record_types() {
231 static $types = array('pub', 'crt', 'crs', 'sub', 'sec', 'ssb', 'uid', 'uat', 'sig', 'rev', 'fpr', 'pkd', 'grp', 'rvk', 'tru', 'spk');
232 return array_combine($types, $types);
233 }
234
235 /**
236 * Returns the key algorithms supported by GnuPG.
237 */
238 function gnupg_get_key_algorithms() {
239 return array(1 => t('RSA'), 16 => t('ElGamal (encrypt)'), 17 => t('DSA (sign)'), 20 => t('ElGamal (sign/encrypt)'));
240 }
241
242 /**
243 * Returns an array of key IDs/titles of the keys in the public keyring.
244 */
245 function gnupg_get_keys($op = 'titles', $key_id = '') {
246 static $columns = array('type', 'validity', 'length', 'algorithm', 'id', 'created_at', 'expires_at', 'local_id', 'trust', 'user_id', 'signature_class', 'capabilities', '..');
247
248 $keys = array();
249 if (($output = gnupg_exec(array('list-public-keys' => TRUE, 'with-colons' => TRUE, $key_id))) && is_array($output)) {
250 foreach ($output as $key) {
251 $key = explode(':', $key);
252 if ($key[0] == 'pub') { // public keys only
253 $key = (object)array_combine($columns, $key);
254 $keys[$key->id] = ($op == 'titles' ? (substr($key->id, -8) . ' ' . $key->user_id . ' (' . $key->created_at . ')') : $key);
255 }
256 }
257 }
258 return $keys;
259 }
260
261 function gnupg_delete_key($key_id, array $options = array()) {
262 // TODO
263 }
264
265 function gnupg_export_key($key_id, array $options = array()) {
266 if (($output = gnupg_exec(array('armor' => TRUE, 'export' => $key_id))) && !empty($output)) {
267 return implode("\n", $output);
268 }
269 }
270
271 function gnupg_import_key($key_data, array $options = array()) {
272 if (($key_ids = gnupg_import_keys($key_data, $options)) && is_array($key_ids) && !empty($key_ids)) {
273 list($key_id) = array_keys($key_ids);
274 return $key_id;
275 }
276 }
277
278 function gnupg_import_keys($key_data, array $options = array()) {
279 $options = array_merge(array('quiet' => FALSE, 'import' => TRUE), $options);
280 if (($process = gnupg_exec($options, TRUE, $pipes)) && is_resource($process)) {
281 list($stdin, $stdout, $stderr) = $pipes;
282
283 foreach (!is_array($key_data) ? array($key_data) : $key_data as $key_text) {
284 fwrite($stdin, $key_text);
285 fwrite($stdin, "\n");
286 }
287 fclose($stdin);
288 $out = stream_get_contents($stdout);
289 fclose($stdout);
290 $err = stream_get_contents($stderr);
291 fclose($stderr);
292
293 if (($result = proc_close($process)) === 0) {
294 $key_ids = array();
295 foreach (explode("\n", $err) as $line) {
296 if (preg_match('/^\w+: key ([0-9A-Fa-f]+):[^"]+"([^"]*)"/', $err, $match)) {
297 list(, $key_id, $user_id) = $match;
298
299 // Attempt to expand the returned 64-bit key IDs to the full key fingerprints:
300 if (($keys = gnupg_get_keys(NULL, $key_id))) {
301 foreach ($keys as $key_fp => $key) {
302 if ((strpos($key_fp, $key_id) === strlen($key_fp) - strlen($key_id)) && $key->user_id == $user_id) {
303 $key_id = $key_fp;
304 break;
305 }
306 }
307 }
308
309 $key_ids[$key_id] = $user_id;
310 }
311 }
312 return $key_ids;
313 }
314
315 return FALSE;
316 }
317 }
318
319 /**
320 * Encrypts the given plaintext using the specified public key ID(s).
321 */
322 function gnupg_encrypt($plaintext, $key_id = GNUPG_KEYID, array $options = array()) {
323 if (!gnupg_is_available()) {
324 return FALSE;
325 }
326
327 // Our trust database is not going to be much use, so tell GnuPG to
328 // always trust. The way to do this changed around version 1.2.3.
329 $options = array_merge(version_compare(gnupg_version(), '1.2.3', '<') ? array('always-trust' => TRUE) : array('trust-model' => 'always'), $options);
330
331 // Encrypt for one or multiple recipients:
332 if ($key_id) {
333 $options['recipient'] = $key_id;
334 }
335
336 $options = array_merge(array('armor' => TRUE, 'encrypt' => TRUE), $options);
337 if (($process = gnupg_exec($options, TRUE, $pipes)) && is_resource($process)) {
338 list($stdin, $stdout, $stderr) = $pipes;
339
340 fwrite($stdin, $plaintext);
341 fclose($stdin);
342 $ciphertext = stream_get_contents($stdout);
343 fclose($stdout);
344 $GLOBALS['gnupg_output'] = stream_get_contents($stderr);
345 fclose($stderr);
346
347 $result = proc_close($process);
348 return $result === 0 ? $ciphertext : FALSE;
349 }
350 }
351
352 /**
353 * Decrypts the given ciphertext using the specified key ID.
354 */
355 function gnupg_decrypt($ciphertext, $key_id = GNUPG_KEYID, array $options = array()) {
356 if (!gnupg_is_available()) {
357 return FALSE;
358 }
359
360 $options = array_merge(array('armor' => TRUE, 'decrypt' => TRUE), $options);
361 if (($process = gnupg_exec($options, TRUE, $pipes)) && is_resource($process)) {
362 list($stdin, $stdout, $stderr) = $pipes;
363
364 fwrite($stdin, $ciphertext);
365 fclose($stdin);
366 $plaintext = stream_get_contents($stdout);
367 fclose($stdout);
368 $GLOBALS['gnupg_output'] = stream_get_contents($stderr);
369 fclose($stderr);
370
371 $result = proc_close($process);
372 return $result === 0 ? $plaintext : FALSE;
373 }
374 }
375
376 //////////////////////////////////////////////////////////////////////////////
377 // GnuPG API internals
378
379 /**
380 * Invokes the 'gpg' executable using exec()/proc_open().
381 */
382 function gnupg_exec(array $options, $proc_open = FALSE, array &$pipes = NULL) {
383 $options = array_merge(array('no-verbose' => TRUE, 'batch' => TRUE, 'quiet' => TRUE, 'no-permission-warning' => TRUE, 'homedir' => GNUPG_HOMEDIR), $options);
384
385 if (!is_writable(GNUPG_HOMEDIR)) {
386 $options['no-random-seed-file'] = TRUE; // prevent the need to write .gnupg/random_seed
387 }
388
389 // Assemble the full command line to be invoked:
390 $command = variable_get('gnupg_exec', gnupg_guess_binpath());
391 foreach ($options as $k => $vs) {
392 foreach (is_array($vs) ? $vs : array($vs) as $v) {
393 if ($v !== FALSE) {
394 $command .= !is_numeric($k) ?
395 ' --'. $k . ($v === TRUE ? '' : ' ' . escapeshellarg((string)$v)) :
396 ' ' . escapeshellarg((string)$v);
397 }
398 }
399 }
400 $arguments = array_slice(func_get_args(), 3);
401 $command = trim($command .' '. implode(' ', array_map('escapeshellarg', $arguments)));
402
403 unset($GLOBALS['gnupg_output']);
404 if (!$proc_open) {
405 exec($command, $output, $result);
406 $GLOBALS['gnupg_output'] = implode("\n", $output);
407 return $result === 0 ? $output : FALSE;
408 }
409 else {
410 return proc_open($command, array(0 => array('pipe', 'r'), 1 => array('pipe', 'w'), 2 => array('pipe', 'w')), $pipes);
411 }
412 }
413
414 /**
415 * Attempts to locate the 'gpg' binary in $PATH and, failing that, in
416 * various well-known locations.
417 */
418 function gnupg_guess_binpath($binary = 'gpg') {
419 if (strtoupper(substr(PHP_OS, 0, 3)) == 'WIN') {
420 return $binary; // let's not even go there
421 }
422 else {
423 $path = exec('type -P '. escapeshellarg($binary), $output, $result);
424 if ($result === 0) {
425 return $path;
426 }
427 else {
428 foreach (array('/opt/local/bin', '/usr/bin', '/usr/local/bin') as $location) {
429 if (@is_executable($path = $location . '/' . $binary)) {
430 return $path;
431 }
432 }
433 return $binary;
434 }
435 }
436 }
437
438 /**
439 * Attempts to make secure the GnuPG home directory by setting access
440 * permissions and writing a .htaccess file.
441 */
442 function gnupg_secure_homedir($directory = GNUPG_HOMEDIR, $form_item = NULL) {
443 // Check if the directory exists, and attempt to automatically create if it doesn't:
444 if (!is_dir($directory)) {
445 if (@mkdir($directory)) {
446 drupal_set_message(t('The GnuPG home directory %directory has been created.', array('%directory' => $directory)));
447 }
448 else {
449 if ($form_item) {
450 form_set_error($form_item, t('The GnuPG home directory %directory does not exist and could not be created.', array('%directory' => $directory)));
451 }
452 return FALSE;
453 }
454 }
455
456 // Prevent world-readability of the directory, if possible:
457 if (!@chmod($directory, 0770)) { // TODO: should probably shell out to 'chmod' to just do an 'o-rwx'
458 $warning = "Security warning: Couldn't set access permissions on the GnuPG home directory %directory. Please secure it manually to prevent access by any other user than the web server process.";
459 drupal_set_message(t($warning, array('%directory' => $directory)), 'warning');
460 watchdog('security', $warning, array('%directory' => $directory), WATCHDOG_WARNING);
461 }
462
463 // Check if the directory is writable. Since GnuPG is executed using our
464 // uid, things will fail if GnuPG can't create the keyring files. On the
465 // other hand, the necessary files may already exist and the administrator
466 // has simply decided to be cautious and let the web server process have
467 // only read access to the directory. This would not be a show-stopper,
468 // and simply means showing an unnecessary warning.
469 if (!is_writable($directory)) {
470 $warning = "Security notice: The GnuPG home directory %directory is not writable by the web server process. Unless all the keyring files required by GnuPG already exist, this may prevent some GnuPG API operations from functioning correctly.";
471 drupal_set_message(t($warning, array('%directory' => $directory)), 'warning');
472 }
473
474 // Attempt to create a .htaccess file to prevent any web access to the
475 // directory. Any prudent administrator will have placed the GnuPG home
476 // directory outside the web root, in which case this isn't necessary and
477 // may produce an unnecessary warning. However, to every savvy user
478 // there will be a correspondingly larger number of careless ones.
479 if (!is_file($directory .'/.htaccess')) {
480 $htaccess_lines = "SetHandler Drupal_Security_Do_Not_Remove\nOptions None\nOptions +FollowSymLinks\nDeny from all\n";
481 if (is_writable($directory) && @file_put_contents($directory .'/.htaccess', $htaccess_lines) > 0) {
482 @chmod($directory .'/.htaccess', 0660);
483 }
484 else {
485 $warning = "Security warning: Couldn't write a <code>.htaccess</code> file in the GnuPG home directory. Please create a %htaccess file with the following contents: <code>!htaccess</code>";
486 $variables = array('%directory' => $directory, '%htaccess' => $directory .'/.htaccess', '!htaccess' => '<br/>'. nl2br(check_plain($htaccess_lines)));
487 drupal_set_message(t($warning, $variables), 'warning');
488 watchdog('security', $warning, $variables, WATCHDOG_WARNING);
489 }
490 }
491
492 return TRUE;
493 }
494
495 //////////////////////////////////////////////////////////////////////////////
496 // Menu callbacks
497
498 function gnupg_user_page_access($account) {
499 return user_access('access user profiles') && !empty($account->gnupg_display_key) && gnupg_get_user_key_data($account->uid);
500 }
501
502 function gnupg_user_page($account) {
503 drupal_set_title(t("@name's public key", array('@name' => $account->name)));
504 return '<div class="gnupg public-key"><pre>' . gnupg_get_user_key_data($account->uid) . '</pre></div>';
505 }

  ViewVC Help
Powered by ViewVC 1.1.2