[libsecret/wip/dueno/local-file: 1/4] egg: Add egg-jwe module for file encryption



commit e83815f551557f13ab53f290668fa041e95916af
Author: Daiki Ueno <dueno src gnome org>
Date:   Thu Sep 20 15:21:24 2018 +0200

    egg: Add egg-jwe module for file encryption

 configure.ac    |   6 +
 egg/Makefile.am |   7 +-
 egg/egg-jwe.c   | 440 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 egg/egg-jwe.h   |  46 ++++++
 egg/test-jwe.c  | 136 ++++++++++++++++++
 5 files changed, 634 insertions(+), 1 deletion(-)
---
diff --git a/configure.ac b/configure.ac
index c4d2b89..aed99bf 100644
--- a/configure.ac
+++ b/configure.ac
@@ -159,6 +159,12 @@ fi
 
 AM_CONDITIONAL(WITH_GCRYPT, test "$enable_gcrypt" = "yes")
 
+# --------------------------------------------------------------------
+# json-glib
+PKG_CHECK_MODULES(JSON_GLIB, json-glib-1.0)
+LIBS="$LIBS $JSON_GLIB_LIBS"
+CFLAGS="$CFLAGS $JSON_GLIB_CFLAGS"
+
 # --------------------------------------------------------------------
 # Compilation options
 
diff --git a/egg/Makefile.am b/egg/Makefile.am
index 2c647f7..c26708f 100644
--- a/egg/Makefile.am
+++ b/egg/Makefile.am
@@ -6,6 +6,7 @@ EXTRA_DIST += egg/egg-testing.h
 if WITH_GCRYPT
 ENCRYPTION_SRCS = egg/egg-dh.c egg/egg-dh.h
 ENCRYPTION_SRCS += egg/egg-hkdf.c egg/egg-hkdf.h
+ENCRYPTION_SRCS += egg/egg-jwe.c egg/egg-jwe.h
 ENCRYPTION_SRCS += egg/egg-libgcrypt.c egg/egg-libgcrypt.h
 else
 ENCRYPTION_SRCS =
@@ -21,6 +22,7 @@ libegg_la_SOURCES = \
 egg_LIBS =  \
        libegg.la \
        $(LIBGCRYPT_LIBS) \
+       $(JSON_GLIB_LIBS) \
        $(GLIB_LIBS)
 
 egg_TESTS = \
@@ -38,11 +40,14 @@ test_secmem_SOURCES = egg/test-secmem.c
 test_secmem_LDADD = $(egg_LIBS)
 
 if WITH_GCRYPT
-egg_TESTS += test-hkdf test-dh
+egg_TESTS += test-hkdf test-dh test-jwe
 
 test_hkdf_SOURCES = egg/test-hkdf.c
 test_hkdf_LDADD = $(egg_LIBS)
 
+test_jwe_SOURCES = egg/test-jwe.c
+test_jwe_LDADD = $(egg_LIBS)
+
 test_dh_SOURCES = egg/test-dh.c
 test_dh_LDADD = $(egg_LIBS)
 endif
diff --git a/egg/egg-jwe.c b/egg/egg-jwe.c
new file mode 100644
index 0000000..715c33e
--- /dev/null
+++ b/egg/egg-jwe.c
@@ -0,0 +1,440 @@
+/*
+ * libsecret
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ *
+ * Author: Daiki Ueno
+ */
+
+#include "config.h"
+
+#include "egg-jwe.h"
+#include "egg-base64.h"
+
+#include <gcrypt.h>
+
+/* IV size is always 12 when GCM */
+#define IV_SIZE 12
+#define TAG_SIZE 16
+
+static gint
+enc_to_cipher (const gchar *enc)
+{
+       if (g_str_equal (enc, "A128GCM"))
+               return GCRY_CIPHER_AES128;
+       else if (g_str_equal (enc, "A192GCM"))
+               return GCRY_CIPHER_AES192;
+       else if (g_str_equal (enc, "A256GCM"))
+               return GCRY_CIPHER_AES256;
+
+       /* FIXME: support CBC ciphersuites */
+       return -1;
+}
+
+JsonNode *
+egg_jwe_symmetric_encrypt (const guchar  *input,
+                           gsize          n_input,
+                           const gchar   *enc,
+                           const guchar  *key,
+                           gsize          n_key,
+                           const guchar   *iv,
+                           gsize          n_iv,
+                           GError       **error)
+{
+       gcry_cipher_hd_t cipher;
+       gcry_error_t gcry;
+       guchar random[IV_SIZE];
+       guchar tag[TAG_SIZE];
+       JsonBuilder *builder;
+       JsonGenerator *generator;
+       JsonNode *result;
+       gchar *protected;
+       gchar *encoded;
+       guchar *ciphertext;
+       gint algo;
+       gsize length;
+
+       algo = enc_to_cipher (enc);
+       if (algo < 0) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "unknown encryption algorithm");
+               return NULL;
+       }
+
+       gcry = gcry_cipher_open (&cipher, algo, GCRY_CIPHER_MODE_GCM, 0);
+       if (gcry) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_FAILED,
+                                    "couldn't open cipher");
+               return NULL;
+       }
+
+       gcry = gcry_cipher_setkey (cipher, key, n_key);
+       if (gcry) {
+               gcry_cipher_close (cipher);
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_FAILED,
+                                    "couldn't set key");
+               return NULL;
+       }
+
+       if (!iv) {
+               gcry_randomize (random, IV_SIZE, GCRY_STRONG_RANDOM);
+               iv = random;
+               n_iv = IV_SIZE;
+       }
+       gcry = gcry_cipher_setiv (cipher, iv, n_iv);
+       if (gcry) {
+               gcry_cipher_close (cipher);
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_FAILED,
+                                    "couldn't set IV");
+               return NULL;
+       }
+
+       /* generate protected header */
+       builder = json_builder_new ();
+       json_builder_begin_object (builder);
+       json_builder_set_member_name (builder, "enc");
+       json_builder_add_string_value (builder, enc);
+       json_builder_end_object (builder);
+       result = json_builder_get_root (builder);
+       g_object_unref (builder);
+
+       generator = json_generator_new ();
+       json_generator_set_root (generator, result);
+       json_node_unref (result);
+       protected = json_generator_to_data (generator, &length);
+       g_object_unref (generator);
+       encoded = egg_base64_encode ((const guchar *) protected, length);
+       g_free (protected);
+       protected = encoded;
+
+       gcry = gcry_cipher_authenticate (cipher, protected, strlen (protected));
+       if (gcry) {
+               g_free (protected);
+               gcry_cipher_close (cipher);
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_FAILED,
+                                    "couldn't set authentication data");
+               return NULL;
+       }
+
+       ciphertext = g_new (guchar, n_input);
+       gcry = gcry_cipher_encrypt (cipher, ciphertext, n_input, input, n_input);
+       if (gcry) {
+               g_free (protected);
+               gcry_cipher_close (cipher);
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_FAILED,
+                                    "couldn't encrypt data");
+               return NULL;
+       }
+
+       gcry = gcry_cipher_gettag (cipher, tag, TAG_SIZE);
+       if (gcry) {
+               g_free (protected);
+               gcry_cipher_close (cipher);
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_FAILED,
+                                    "couldn't get tag");
+               return NULL;
+       }
+
+       gcry_cipher_close (cipher);
+
+       builder = json_builder_new ();
+       json_builder_begin_object (builder);
+
+       json_builder_set_member_name (builder, "ciphertext");
+       encoded = egg_base64_encode (ciphertext, n_input);
+       g_free (ciphertext);
+       json_builder_add_string_value (builder, encoded);
+       g_free (encoded);
+
+       json_builder_set_member_name (builder, "encrypted_key");
+       json_builder_add_string_value (builder, "");
+
+       json_builder_set_member_name (builder, "iv");
+       encoded = egg_base64_encode (iv, IV_SIZE);
+       json_builder_add_string_value (builder, encoded);
+       g_free (encoded);
+
+       json_builder_set_member_name (builder, "tag");
+       encoded = egg_base64_encode (tag, TAG_SIZE);
+       json_builder_add_string_value (builder, encoded);
+       g_free (encoded);
+
+       json_builder_set_member_name (builder, "protected");
+       json_builder_add_string_value (builder, protected);
+       g_free (protected);
+
+       json_builder_set_member_name (builder, "header");
+       json_builder_begin_object (builder);
+       json_builder_set_member_name (builder, "alg");
+       json_builder_add_string_value (builder, "dir");
+       json_builder_end_object (builder);
+
+       json_builder_end_object (builder);
+       result = json_builder_get_root (builder);
+       g_object_unref (builder);
+
+       return result;
+}
+
+guchar *
+egg_jwe_symmetric_decrypt (JsonNode      *root,
+                           const guchar  *key,
+                           gsize          n_key,
+                           gsize         *length,
+                           GError       **error)
+{
+       gcry_cipher_hd_t cipher;
+       gcry_error_t gcry;
+       guchar iv[(IV_SIZE / 3 + 1) *4 + 1];
+       guchar tag[(TAG_SIZE / 3 + 1) * 4 + 1];
+       JsonParser *parser;
+       JsonObject *object;
+       const gchar *string;
+       const gchar *protected;
+       JsonNode *protected_root;
+       JsonObject *protected_object;
+       gchar *buffer;
+       guchar *decoded;
+       gsize n_decoded;
+       gint algo;
+       gboolean ret;
+
+       object = json_node_get_object (root);
+       if (!object) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "the root element is not an object");
+               return NULL;
+       }
+
+       protected = json_object_get_string_member (object, "protected");
+       if (!protected) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "the root element doesn't contain \"protected\" element");
+               return NULL;
+       }
+
+       buffer = g_strdup (protected);
+       decoded = egg_base64_decode_inplace (buffer, &n_decoded);
+       if (!decoded) {
+               g_free (buffer);
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "couldn't decode \"protected\" element");
+               return NULL;
+       }
+
+       parser = json_parser_new ();
+       ret = json_parser_load_from_data (parser, (gchar *) decoded, n_decoded, error);
+       g_free (buffer);
+       if (!ret) {
+               g_object_unref (parser);
+               return NULL;
+       }
+
+       protected_root = json_parser_steal_root (parser);
+       g_object_unref (parser);
+
+       protected_object = json_node_get_object (protected_root);
+       if (!protected_object) {
+               json_node_unref (protected_root);
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "the \"protected\" element is not an object");
+               return NULL;
+       }
+
+       string = json_object_get_string_member (protected_object, "enc");
+       if (!string) {
+               json_node_unref (protected_root);
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "the \"protected\" element doesn't contain \"enc\"");
+               return NULL;
+       }
+
+       algo = enc_to_cipher (string);
+       json_node_unref (protected_root);
+       if (algo < 0) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "unknown encryption algorithm");
+               return NULL;
+       }
+
+       string = json_object_get_string_member (object, "iv");
+       if (!string) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "the root element doesn't contain \"iv\" element");
+               return NULL;
+       }
+       if (strlen (string) > sizeof (iv) - 1) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "IV is too large");
+               return NULL;
+       }
+
+       memset (iv, 0, sizeof (iv));
+       memcpy (iv, string, strlen (string));
+       decoded = egg_base64_decode_inplace ((gchar *) iv, &n_decoded);
+       if (!decoded) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "couldn't decode \"iv\" element");
+               return NULL;
+       }
+
+       string = json_object_get_string_member (object, "tag");
+       if (!string) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "the root element doesn't contain \"tag\" element");
+               return NULL;
+       }
+       if (strlen (string) > sizeof (tag) - 1) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "tag is too large");
+               return NULL;
+       }
+
+       memset (tag, 0, sizeof (tag));
+       memcpy (tag, string, strlen (string));
+       decoded = egg_base64_decode_inplace ((gchar *) tag, &n_decoded);
+       if (!decoded) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "couldn't decode \"tag\" element");
+               return NULL;
+       }
+
+       string = json_object_get_string_member (object, "ciphertext");
+       if (!string) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "the root element doesn't contain \"ciphertext\"");
+               return NULL;
+       }
+
+       buffer = g_strdup (string);
+       decoded = egg_base64_decode_inplace (buffer, &n_decoded);
+       if (!decoded) {
+               g_free (buffer);
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_INVALID_ARGUMENT,
+                                    "couldn't decode \"ciphertext\" element");
+               return NULL;
+       }
+
+       gcry = gcry_cipher_open (&cipher, algo, GCRY_CIPHER_MODE_GCM, 0);
+       if (gcry) {
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_FAILED,
+                                    "couldn't open cipher");
+               return NULL;
+       }
+
+       gcry = gcry_cipher_setkey (cipher, key, n_key);
+       if (gcry) {
+               gcry_cipher_close (cipher);
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_FAILED,
+                                    "couldn't set key");
+               return NULL;
+       }
+
+       gcry = gcry_cipher_setiv (cipher, iv, IV_SIZE);
+       if (gcry) {
+               gcry_cipher_close (cipher);
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_FAILED,
+                                    "couldn't set IV");
+               return NULL;
+       }
+
+       gcry = gcry_cipher_authenticate (cipher, protected, strlen (protected));
+       if (gcry) {
+               gcry_cipher_close (cipher);
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_FAILED,
+                                    "couldn't set authentication data");
+               return NULL;
+       }
+
+       gcry = gcry_cipher_decrypt (cipher, decoded, n_decoded, decoded, n_decoded);
+       if (gcry) {
+               g_free (decoded);
+               gcry_cipher_close (cipher);
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_FAILED,
+                                    "couldn't decrypt data");
+               return NULL;
+       }
+
+       gcry = gcry_cipher_checktag (cipher, tag, TAG_SIZE);
+       if (gcry) {
+               g_free (decoded);
+               gcry_cipher_close (cipher);
+               g_set_error_literal (error,
+                                    G_IO_ERROR,
+                                    G_IO_ERROR_FAILED,
+                                    "couldn't check tag");
+               return NULL;
+       }
+
+       gcry_cipher_close (cipher);
+
+       *length = n_decoded;
+       return decoded;
+}
diff --git a/egg/egg-jwe.h b/egg/egg-jwe.h
new file mode 100644
index 0000000..c0d2b4c
--- /dev/null
+++ b/egg/egg-jwe.h
@@ -0,0 +1,46 @@
+/*
+ * libsecret
+ *
+ * Copyright (C) 2018 Red Hat, Inc.
+ *
+ * This program is free software; you can redistribute it and/or modify
+ * it under the terms of the GNU Lesser General Public License as
+ * published by the Free Software Foundation; either version 2.1 of
+ * the License, or (at your option) any later version.
+ *
+ * This program 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
+ * Lesser General Public License for more details.
+ *
+ * You should have received a copy of the GNU Lesser General Public
+ * License along with this program; if not, write to the Free Software
+ * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
+ * MA 02110-1301 USA
+ *
+ * Author: Daiki Ueno
+ */
+
+#ifndef EGG_JWE_H_
+#define EGG_JWE_H_
+
+#include <json-glib/json-glib.h>
+
+JsonNode *
+egg_jwe_symmetric_encrypt (const guchar  *input,
+                           gsize          n_input,
+                           const gchar   *enc,
+                           const guchar  *key,
+                           gsize          n_key,
+                           const guchar  *iv,
+                           gsize          n_iv,
+                           GError       **error);
+
+guchar *
+egg_jwe_symmetric_decrypt (JsonNode      *root,
+                           const guchar  *key,
+                           gsize          n_key,
+                           gsize         *length,
+                           GError       **error);
+
+#endif
diff --git a/egg/test-jwe.c b/egg/test-jwe.c
new file mode 100644
index 0000000..898d745
--- /dev/null
+++ b/egg/test-jwe.c
@@ -0,0 +1,136 @@
+/* test-base64.c: Test egg-base64.c
+
+   Copyright (C) 2018 Red Hat, Inc.
+
+   The Gnome Keyring Library is free software; you can redistribute it and/or
+   modify it under the terms of the GNU Library General Public License as
+   published by the Free Software Foundation; either version 2 of the
+   License, or (at your option) any later version.
+
+   The Gnome Keyring Library 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
+   Library General Public License for more details.
+
+   You should have received a copy of the GNU Library General Public
+   License along with the Gnome Library; see the file COPYING.LIB.  If not,
+   see <http://www.gnu.org/licenses/>.
+
+   Author: Daiki Ueno
+*/
+
+#include "config.h"
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+
+#include "egg/egg-jwe.h"
+#include "egg/egg-base64.h"
+#include "egg/egg-testing.h"
+
+#define PLAINTEXT "test test\n"
+#define KEY "7IYHpL3E0SApQ3Uk58_Liw"
+#define IV "aeZrw-VuRzycKDEu"
+#define CIPHERTEXT "sWMnFnG4OcjdpA"
+#define TAG "Jx1MqdYjb2n-0-zXTGUHZw"
+#define PROTECTED "eyJlbmMiOiJBMTI4R0NNIn0"
+
+#define MSG "{\"ciphertext\":\"" CIPHERTEXT "\"," \
+       "\"encrypted_key\":\"\"," \
+       "\"header\":{\"alg\":\"dir\"}," \
+       "\"iv\":\"" IV "\"," \
+       "\"protected\":\"" PROTECTED "\"," \
+       "\"tag\":\"" TAG "\"}"
+
+static void
+test_symmetric_encrypt (void)
+{
+       gchar *buffer;
+       guchar *key;
+       gsize n_key;
+       guchar *iv;
+       gsize n_iv;
+       GError *error;
+       JsonNode *root;
+       JsonObject *object;
+       const gchar *string;
+
+       buffer = g_strdup (KEY);
+       key = egg_base64_decode_inplace (buffer, &n_key);
+       buffer = g_strdup (IV);
+       iv = egg_base64_decode_inplace (buffer, &n_iv);
+       error = NULL;
+       root = egg_jwe_symmetric_encrypt ((const guchar *) PLAINTEXT,
+                                         sizeof (PLAINTEXT)-1,
+                                         "A128GCM", key, n_key, iv, n_iv,
+                                         &error);
+       g_assert_nonnull (root);
+       g_assert_no_error (error);
+       g_free (key);
+       g_free (iv);
+
+       object = json_node_get_object (root);
+       g_assert_nonnull (object);
+
+       string = json_object_get_string_member (object, "ciphertext");
+       g_assert_cmpstr (string, ==, CIPHERTEXT);
+       string = json_object_get_string_member (object, "iv");
+       g_assert_cmpstr (string, ==, IV);
+       string = json_object_get_string_member (object, "tag");
+       g_assert_cmpstr (string, ==, TAG);
+       string = json_object_get_string_member (object, "protected");
+       g_assert_cmpstr (string, ==, PROTECTED);
+       json_node_unref (root);
+}
+
+static void
+test_symmetric_decrypt (void)
+{
+       JsonParser *parser;
+       JsonNode *root;
+       gchar *buffer;
+       guchar *key;
+       gsize n_key;
+       guchar *plaintext;
+       gsize n_plaintext;
+       GError *error;
+       gboolean ret;
+
+       parser = json_parser_new ();
+       error = NULL;
+       ret = json_parser_load_from_data (parser, MSG, sizeof (MSG)-1, &error);
+       g_assert_true (ret);
+       g_assert_no_error (error);
+
+       buffer = g_strdup (KEY);
+       key = egg_base64_decode_inplace (buffer, &n_key);
+
+       root = json_parser_steal_root (parser);
+       g_object_unref (parser);
+
+       error = NULL;
+       plaintext = egg_jwe_symmetric_decrypt (root, key, n_key, &n_plaintext,
+                                              &error);
+       g_assert_nonnull (plaintext);
+       g_assert_no_error (error);
+
+       g_free (key);
+       json_node_unref (root);
+       g_assert_nonnull (plaintext);
+       g_assert_no_error (error);
+
+       g_assert_cmpmem (plaintext, n_plaintext, PLAINTEXT, sizeof(PLAINTEXT)-1);
+       g_free (plaintext);
+}
+
+int
+main (int argc, char **argv)
+{
+       g_test_init (&argc, &argv, NULL);
+
+       g_test_add_func ("/jwe/test-symmetric-encrypt", test_symmetric_encrypt);
+       g_test_add_func ("/jwe/test-symmetric-decrypt", test_symmetric_decrypt);
+
+       return g_test_run ();
+}


[Date Prev][Date Next]   [Thread Prev][Thread Next]   [Thread Index] [Date Index] [Author Index]