/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Copyright (C) 1999-2008 Novell, Inc. (www.novell.com) * * This library 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. * * This 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 Lesser General Public License * for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see . * * Authors: Jeffrey Stedfast */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include "camel-charset-map.h" #include "camel-iconv.h" #include "camel-mime-utils.h" #include "camel-net-utils.h" #include "camel-network-settings.h" #ifdef G_OS_WIN32 #include #include #ifdef HAVE_WSPIAPI_H #include #endif #endif #include "camel-sasl-digest-md5.h" #define d(x) #define PARANOID(x) x #define CAMEL_SASL_DIGEST_MD5_GET_PRIVATE(obj) \ (G_TYPE_INSTANCE_GET_PRIVATE \ ((obj), CAMEL_TYPE_SASL_DIGEST_MD5, CamelSaslDigestMd5Private)) /* Implements rfc2831 */ static CamelServiceAuthType sasl_digest_md5_auth_type = { N_("DIGEST-MD5"), N_("This option will connect to the server using a " "secure DIGEST-MD5 password, if the server supports it."), "DIGEST-MD5", TRUE }; enum { STATE_AUTH, STATE_FINAL }; typedef struct { const gchar *name; guint type; } DataType; enum { DIGEST_REALM, DIGEST_NONCE, DIGEST_QOP, DIGEST_STALE, DIGEST_MAXBUF, DIGEST_CHARSET, DIGEST_ALGORITHM, DIGEST_CIPHER, DIGEST_UNKNOWN }; static DataType digest_args[] = { { "realm", DIGEST_REALM }, { "nonce", DIGEST_NONCE }, { "qop", DIGEST_QOP }, { "stale", DIGEST_STALE }, { "maxbuf", DIGEST_MAXBUF }, { "charset", DIGEST_CHARSET }, { "algorithm", DIGEST_ALGORITHM }, { "cipher", DIGEST_CIPHER }, { NULL, DIGEST_UNKNOWN } }; #define QOP_AUTH (1 << 0) #define QOP_AUTH_INT (1 << 1) #define QOP_AUTH_CONF (1 << 2) #define QOP_INVALID (1 << 3) static DataType qop_types[] = { { "auth", QOP_AUTH }, { "auth-int", QOP_AUTH_INT }, { "auth-conf", QOP_AUTH_CONF }, { NULL, QOP_INVALID } }; #define CIPHER_DES (1 << 0) #define CIPHER_3DES (1 << 1) #define CIPHER_RC4 (1 << 2) #define CIPHER_RC4_40 (1 << 3) #define CIPHER_RC4_56 (1 << 4) #define CIPHER_INVALID (1 << 5) static DataType cipher_types[] = { { "des", CIPHER_DES }, { "3des", CIPHER_3DES }, { "rc4", CIPHER_RC4 }, { "rc4-40", CIPHER_RC4_40 }, { "rc4-56", CIPHER_RC4_56 }, { NULL, CIPHER_INVALID } }; struct _param { gchar *name; gchar *value; }; struct _DigestChallenge { GPtrArray *realms; gchar *nonce; guint qop; gboolean stale; gint32 maxbuf; gchar *charset; gchar *algorithm; guint cipher; GList *params; }; struct _DigestURI { gchar *type; gchar *host; gchar *name; }; struct _DigestResponse { gchar *username; gchar *realm; gchar *nonce; gchar *cnonce; gchar nc[9]; guint qop; struct _DigestURI *uri; gchar resp[33]; guint32 maxbuf; gchar *charset; guint cipher; gchar *authzid; gchar *param; }; struct _CamelSaslDigestMd5Private { struct _DigestChallenge *challenge; struct _DigestResponse *response; gint state; }; G_DEFINE_TYPE (CamelSaslDigestMd5, camel_sasl_digest_md5, CAMEL_TYPE_SASL) static void decode_lwsp (const gchar **in) { const gchar *inptr = *in; while (isspace (*inptr)) inptr++; *in = inptr; } static gchar * decode_quoted_string (const gchar **in) { const gchar *inptr = *in; gchar *out = NULL, *outptr; gint outlen; gint c; decode_lwsp (&inptr); if (*inptr == '"') { const gchar *intmp; gint skip = 0; /* first, calc length */ inptr++; intmp = inptr; while ((c = *intmp++) && c != '"') { if (c == '\\' && *intmp) { intmp++; skip++; } } outlen = intmp - inptr - skip; out = outptr = g_malloc (outlen + 1); while ((c = *inptr++) && c != '"') { if (c == '\\' && *inptr) { c = *inptr++; } *outptr++ = c; } *outptr = '\0'; } *in = inptr; return out; } static gchar * decode_token (const gchar **in) { const gchar *inptr = *in; const gchar *start; decode_lwsp (&inptr); start = inptr; while (*inptr && *inptr != '=' && *inptr != ',') inptr++; if (inptr > start) { *in = inptr; return g_strndup (start, inptr - start); } else { return NULL; } } static gchar * decode_value (const gchar **in) { const gchar *inptr = *in; decode_lwsp (&inptr); if (*inptr == '"') { d (printf ("decoding quoted string token\n")); return decode_quoted_string (in); } else { d (printf ("decoding string token\n")); return decode_token (in); } } static GList * parse_param_list (const gchar *tokens) { GList *params = NULL; struct _param *param; const gchar *ptr; for (ptr = tokens; ptr && *ptr; ) { param = g_new0 (struct _param, 1); param->name = decode_token (&ptr); if (*ptr == '=') { ptr++; param->value = decode_value (&ptr); } params = g_list_prepend (params, param); if (*ptr == ',') ptr++; } return params; } static guint decode_data_type (DataType *dtype, const gchar *name) { gint i; for (i = 0; dtype[i].name; i++) { if (!g_ascii_strcasecmp (dtype[i].name, name)) break; } return dtype[i].type; } #define get_digest_arg(name) decode_data_type (digest_args, name) #define decode_qop(name) decode_data_type (qop_types, name) #define decode_cipher(name) decode_data_type (cipher_types, name) static const gchar * type_to_string (DataType *dtype, guint type) { gint i; for (i = 0; dtype[i].name; i++) { if (dtype[i].type == type) break; } return dtype[i].name; } #define qop_to_string(type) type_to_string (qop_types, type) #define cipher_to_string(type) type_to_string (cipher_types, type) static void digest_abort (gboolean *have_type, gboolean *abort) { if (*have_type) *abort = TRUE; *have_type = TRUE; } static struct _DigestChallenge * parse_server_challenge (const gchar *tokens, gboolean *abort) { struct _DigestChallenge *challenge = NULL; GList *params, *p; const gchar *ptr; #ifdef PARANOID gboolean got_algorithm = FALSE; gboolean got_stale = FALSE; gboolean got_maxbuf = FALSE; gboolean got_charset = FALSE; #endif /* PARANOID */ params = parse_param_list (tokens); if (!params) { *abort = TRUE; return NULL; } *abort = FALSE; challenge = g_new0 (struct _DigestChallenge, 1); challenge->realms = g_ptr_array_new (); challenge->maxbuf = 65536; for (p = params; p; p = p->next) { struct _param *param = p->data; gint type; type = get_digest_arg (param->name); switch (type) { case DIGEST_REALM: for (ptr = param->value; ptr && *ptr; ) { gchar *token; token = decode_token (&ptr); if (token) g_ptr_array_add (challenge->realms, token); if (*ptr == ',') ptr++; } g_free (param->value); g_free (param->name); g_free (param); break; case DIGEST_NONCE: g_free (challenge->nonce); challenge->nonce = param->value; g_free (param->name); g_free (param); break; case DIGEST_QOP: for (ptr = param->value; ptr && *ptr; ) { gchar *token; token = decode_token (&ptr); if (token) challenge->qop |= decode_qop (token); if (*ptr == ',') ptr++; } if (challenge->qop & QOP_INVALID) challenge->qop = QOP_INVALID; g_free (param->value); g_free (param->name); g_free (param); break; case DIGEST_STALE: PARANOID (digest_abort (&got_stale, abort)); if (!g_ascii_strcasecmp (param->value, "true")) challenge->stale = TRUE; else challenge->stale = FALSE; g_free (param->value); g_free (param->name); g_free (param); break; case DIGEST_MAXBUF: PARANOID (digest_abort (&got_maxbuf, abort)); challenge->maxbuf = atoi (param->value); g_free (param->value); g_free (param->name); g_free (param); break; case DIGEST_CHARSET: PARANOID (digest_abort (&got_charset, abort)); g_free (challenge->charset); if (param->value && *param->value) challenge->charset = param->value; else challenge->charset = NULL; g_free (param->name); g_free (param); break; case DIGEST_ALGORITHM: PARANOID (digest_abort (&got_algorithm, abort)); g_free (challenge->algorithm); challenge->algorithm = param->value; g_free (param->name); g_free (param); break; case DIGEST_CIPHER: for (ptr = param->value; ptr && *ptr; ) { gchar *token; token = decode_token (&ptr); if (token) challenge->cipher |= decode_cipher (token); if (*ptr == ',') ptr++; } if (challenge->cipher & CIPHER_INVALID) challenge->cipher = CIPHER_INVALID; g_free (param->value); g_free (param->name); g_free (param); break; default: challenge->params = g_list_prepend (challenge->params, param); break; } } g_list_free (params); return challenge; } static gchar * digest_uri_to_string (struct _DigestURI *uri) { if (uri->name) return g_strdup_printf ("%s/%s/%s", uri->type, uri->host, uri->name); else return g_strdup_printf ("%s/%s", uri->type, uri->host); } static void compute_response (struct _DigestResponse *resp, const gchar *passwd, gboolean client, guchar out[33]) { GString *buffer; GChecksum *checksum; guint8 *digest; gsize length; gchar *hex_a1; gchar *hex_a2; gchar *hex_kd; gchar *uri; buffer = g_string_sized_new (256); length = g_checksum_type_get_length (G_CHECKSUM_MD5); digest = g_alloca (length); /* Compute A1. */ g_string_append (buffer, resp->username); g_string_append_c (buffer, ':'); g_string_append (buffer, resp->realm); g_string_append_c (buffer, ':'); g_string_append (buffer, passwd); checksum = g_checksum_new (G_CHECKSUM_MD5); g_checksum_update ( checksum, (const guchar *) buffer->str, buffer->len); g_checksum_get_digest (checksum, digest, &length); g_checksum_free (checksum); /* Clear the buffer. */ g_string_truncate (buffer, 0); g_string_append_len (buffer, (gchar *) digest, length); g_string_append_c (buffer, ':'); g_string_append (buffer, resp->nonce); g_string_append_c (buffer, ':'); g_string_append (buffer, resp->cnonce); if (resp->authzid != NULL) { g_string_append_c (buffer, ':'); g_string_append (buffer, resp->authzid); } hex_a1 = g_compute_checksum_for_string ( G_CHECKSUM_MD5, buffer->str, buffer->len); /* Clear the buffer. */ g_string_truncate (buffer, 0); /* Compute A2. */ if (client) { /* We are calculating the client response. */ g_string_append (buffer, "AUTHENTICATE:"); } else { /* We are calculating the server rspauth. */ g_string_append_c (buffer, ':'); } uri = digest_uri_to_string (resp->uri); g_string_append (buffer, uri); g_free (uri); if (resp->qop == QOP_AUTH_INT || resp->qop == QOP_AUTH_CONF) g_string_append (buffer, ":00000000000000000000000000000000"); hex_a2 = g_compute_checksum_for_string ( G_CHECKSUM_MD5, buffer->str, buffer->len); /* Clear the buffer. */ g_string_truncate (buffer, 0); /* Compute KD. */ g_string_append (buffer, hex_a1); g_string_append_c (buffer, ':'); g_string_append (buffer, resp->nonce); g_string_append_c (buffer, ':'); g_string_append_len (buffer, resp->nc, 8); g_string_append_c (buffer, ':'); g_string_append (buffer, resp->cnonce); g_string_append_c (buffer, ':'); g_string_append (buffer, qop_to_string (resp->qop)); g_string_append_c (buffer, ':'); g_string_append (buffer, hex_a2); hex_kd = g_compute_checksum_for_string ( G_CHECKSUM_MD5, buffer->str, buffer->len); g_strlcpy ((gchar *) out, hex_kd, 33); g_free (hex_a1); g_free (hex_a2); g_free (hex_kd); g_string_free (buffer, TRUE); } static struct _DigestResponse * generate_response (struct _DigestChallenge *challenge, const gchar *host, const gchar *protocol, const gchar *user, const gchar *passwd) { struct _DigestResponse *resp; struct _DigestURI *uri; GChecksum *checksum; guint8 *digest; gsize length; gchar *bgen; length = g_checksum_type_get_length (G_CHECKSUM_MD5); digest = g_alloca (length); resp = g_new0 (struct _DigestResponse, 1); resp->username = g_strdup (user); /* FIXME: we should use the preferred realm */ if (challenge->realms && challenge->realms->len > 0) resp->realm = g_strdup (challenge->realms->pdata[0]); else resp->realm = g_strdup (""); resp->nonce = g_strdup (challenge->nonce); /* generate the cnonce */ bgen = g_strdup_printf ( "%p:%lu:%lu", (gpointer) resp, (gulong) getpid (), (gulong) time (NULL)); checksum = g_checksum_new (G_CHECKSUM_MD5); g_checksum_update (checksum, (guchar *) bgen, -1); g_checksum_get_digest (checksum, digest, &length); g_checksum_free (checksum); g_free (bgen); /* take our recommended 64 bits of entropy */ resp->cnonce = g_base64_encode ((guchar *) digest, 8); /* we don't support re-auth so the nonce count is always 1 */ g_strlcpy (resp->nc, "00000001", sizeof (resp->nc)); /* choose the QOP */ /* FIXME: choose - probably choose "auth" ??? */ resp->qop = QOP_AUTH; /* create the URI */ uri = g_new0 (struct _DigestURI, 1); uri->type = g_strdup (protocol); uri->host = g_strdup (host); uri->name = NULL; resp->uri = uri; /* charsets... yay */ if (challenge->charset) { /* I believe that this is only ever allowed to be * UTF-8. We strdup the charset specified by the * challenge anyway, just in case it's not UTF-8. */ resp->charset = g_strdup (challenge->charset); } resp->cipher = CIPHER_INVALID; if (resp->qop == QOP_AUTH_CONF) { /* FIXME: choose a cipher? */ resp->cipher = CIPHER_INVALID; } /* we don't really care about this... */ resp->authzid = NULL; compute_response (resp, passwd, TRUE, (guchar *) resp->resp); return resp; } static GByteArray * digest_response (struct _DigestResponse *resp) { GByteArray *buffer; const gchar *str; gchar *buf; buffer = g_byte_array_new (); g_byte_array_append (buffer, (guint8 *) "username=\"", 10); if (resp->charset) { /* Encode the username using the requested charset */ gchar *username, *outbuf; const gchar *charset; gsize len, outlen; const gchar *inbuf; iconv_t cd; charset = camel_iconv_locale_charset (); if (!charset) charset = "iso-8859-1"; cd = camel_iconv_open (resp->charset, charset); len = strlen (resp->username); outlen = 2 * len; /* plenty of space */ outbuf = username = g_malloc0 (outlen + 1); inbuf = resp->username; if (cd == (iconv_t) -1 || camel_iconv (cd, &inbuf, &len, &outbuf, &outlen) == (gsize) -1) { /* We can't convert to UTF-8 - pretend we never got a charset param? */ g_free (resp->charset); resp->charset = NULL; /* Set the username to the non-UTF-8 version */ g_free (username); username = g_strdup (resp->username); } if (cd != (iconv_t) -1) camel_iconv_close (cd); g_byte_array_append (buffer, (guint8 *) username, strlen (username)); g_free (username); } else { g_byte_array_append (buffer, (guint8 *) resp->username, strlen (resp->username)); } g_byte_array_append (buffer, (guint8 *) "\",realm=\"", 9); g_byte_array_append (buffer, (guint8 *) resp->realm, strlen (resp->realm)); g_byte_array_append (buffer, (guint8 *) "\",nonce=\"", 9); g_byte_array_append (buffer, (guint8 *) resp->nonce, strlen (resp->nonce)); g_byte_array_append (buffer, (guint8 *) "\",cnonce=\"", 10); g_byte_array_append (buffer, (guint8 *) resp->cnonce, strlen (resp->cnonce)); g_byte_array_append (buffer, (guint8 *) "\",nc=", 5); g_byte_array_append (buffer, (guint8 *) resp->nc, 8); g_byte_array_append (buffer, (guint8 *) ",qop=", 5); str = qop_to_string (resp->qop); g_byte_array_append (buffer, (guint8 *) str, strlen (str)); g_byte_array_append (buffer, (guint8 *) ",digest-uri=\"", 13); buf = digest_uri_to_string (resp->uri); g_byte_array_append (buffer, (guint8 *) buf, strlen (buf)); g_free (buf); g_byte_array_append (buffer, (guint8 *) "\",response=", 11); g_byte_array_append (buffer, (guint8 *) resp->resp, 32); if (resp->maxbuf > 0) { g_byte_array_append (buffer, (guint8 *) ",maxbuf=", 8); buf = g_strdup_printf ("%u", resp->maxbuf); g_byte_array_append (buffer, (guint8 *) buf, strlen (buf)); g_free (buf); } if (resp->charset) { g_byte_array_append (buffer, (guint8 *) ",charset=", 9); g_byte_array_append (buffer, (guint8 *) resp->charset, strlen ((gchar *) resp->charset)); } if (resp->cipher != CIPHER_INVALID) { str = cipher_to_string (resp->cipher); if (str) { g_byte_array_append (buffer, (guint8 *) ",cipher=\"", 9); g_byte_array_append (buffer, (guint8 *) str, strlen (str)); g_byte_array_append (buffer, (guint8 *) "\"", 1); } } if (resp->authzid) { g_byte_array_append (buffer, (guint8 *) ",authzid=\"", 10); g_byte_array_append (buffer, (guint8 *) resp->authzid, strlen (resp->authzid)); g_byte_array_append (buffer, (guint8 *) "\"", 1); } return buffer; } static void sasl_digest_md5_finalize (GObject *object) { CamelSaslDigestMd5 *sasl = CAMEL_SASL_DIGEST_MD5 (object); struct _DigestChallenge *c = sasl->priv->challenge; struct _DigestResponse *r = sasl->priv->response; GList *p; gint i; if (c != NULL) { for (i = 0; i < c->realms->len; i++) g_free (c->realms->pdata[i]); g_ptr_array_free (c->realms, TRUE); g_free (c->nonce); g_free (c->charset); g_free (c->algorithm); for (p = c->params; p; p = p->next) { struct _param *param = p->data; g_free (param->name); g_free (param->value); g_free (param); } g_list_free (c->params); g_free (c); } if (r != NULL) { g_free (r->username); g_free (r->realm); g_free (r->nonce); g_free (r->cnonce); if (r->uri) { g_free (r->uri->type); g_free (r->uri->host); g_free (r->uri->name); } g_free (r->charset); g_free (r->authzid); g_free (r->param); g_free (r); } /* Chain up to parent's finalize() method. */ G_OBJECT_CLASS (camel_sasl_digest_md5_parent_class)->finalize (object); } static GByteArray * sasl_digest_md5_challenge_sync (CamelSasl *sasl, GByteArray *token, GCancellable *cancellable, GError **error) { CamelSaslDigestMd5 *sasl_digest = CAMEL_SASL_DIGEST_MD5 (sasl); struct _CamelSaslDigestMd5Private *priv = sasl_digest->priv; CamelNetworkSettings *network_settings; CamelSettings *settings; CamelService *service; struct _param *rspauth; GByteArray *ret = NULL; gboolean abort = FALSE; const gchar *ptr; guchar out[33]; gchar *tokens; struct addrinfo *ai, hints; const gchar *service_name; const gchar *password; gchar *host; gchar *user; /* Need to wait for the server */ if (!token) return NULL; service = camel_sasl_get_service (sasl); service_name = camel_sasl_get_service_name (sasl); settings = camel_service_ref_settings (service); g_return_val_if_fail (CAMEL_IS_NETWORK_SETTINGS (settings), NULL); network_settings = CAMEL_NETWORK_SETTINGS (settings); host = camel_network_settings_dup_host (network_settings); user = camel_network_settings_dup_user (network_settings); g_object_unref (settings); g_return_val_if_fail (user != NULL, NULL); if (!host || !*host) { g_free (host); host = g_strdup ("localhost"); } password = camel_service_get_password (service); g_return_val_if_fail (password != NULL, NULL); switch (priv->state) { case STATE_AUTH: if (token->len > 2048) { g_set_error ( error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_CANT_AUTHENTICATE, _("Server challenge too long (>2048 octets)")); goto exit; } tokens = g_strndup ((gchar *) token->data, token->len); priv->challenge = parse_server_challenge (tokens, &abort); g_free (tokens); if (!priv->challenge || abort) { g_set_error ( error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_CANT_AUTHENTICATE, _("Server challenge invalid\n")); goto exit; } if (priv->challenge->qop == QOP_INVALID) { g_set_error ( error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_CANT_AUTHENTICATE, _("Server challenge contained invalid " "\"Quality of Protection\" token")); goto exit; } memset (&hints, 0, sizeof (hints)); hints.ai_flags = AI_CANONNAME; ai = camel_getaddrinfo ( host, NULL, &hints, cancellable, NULL); if (ai && ai->ai_canonname) ptr = ai->ai_canonname; else ptr = "localhost.localdomain"; priv->response = generate_response ( priv->challenge, ptr, service_name, user, password); if (ai) camel_freeaddrinfo (ai); ret = digest_response (priv->response); break; case STATE_FINAL: if (token->len) tokens = g_strndup ((gchar *) token->data, token->len); else tokens = NULL; if (!tokens || !*tokens) { g_free (tokens); g_set_error ( error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_CANT_AUTHENTICATE, _("Server response did not contain " "authorization data")); goto exit; } rspauth = g_new0 (struct _param, 1); ptr = tokens; rspauth->name = decode_token (&ptr); if (*ptr == '=') { ptr++; rspauth->value = decode_value (&ptr); } g_free (tokens); if (!rspauth->value) { g_free (rspauth->name); g_free (rspauth); g_set_error ( error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_CANT_AUTHENTICATE, _("Server response contained incomplete " "authorization data")); goto exit; } compute_response (priv->response, password, FALSE, out); if (memcmp (out, rspauth->value, 32) != 0) { g_free (rspauth->name); g_free (rspauth->value); g_free (rspauth); g_set_error ( error, CAMEL_SERVICE_ERROR, CAMEL_SERVICE_ERROR_CANT_AUTHENTICATE, _("Server response does not match")); camel_sasl_set_authenticated (sasl, TRUE); goto exit; } g_free (rspauth->name); g_free (rspauth->value); g_free (rspauth); ret = g_byte_array_new (); camel_sasl_set_authenticated (sasl, TRUE); default: break; } priv->state++; exit: g_free (host); g_free (user); return ret; } static void camel_sasl_digest_md5_class_init (CamelSaslDigestMd5Class *class) { GObjectClass *object_class; CamelSaslClass *sasl_class; g_type_class_add_private (class, sizeof (CamelSaslDigestMd5Private)); object_class = G_OBJECT_CLASS (class); object_class->finalize = sasl_digest_md5_finalize; sasl_class = CAMEL_SASL_CLASS (class); sasl_class->auth_type = &sasl_digest_md5_auth_type; sasl_class->challenge_sync = sasl_digest_md5_challenge_sync; } static void camel_sasl_digest_md5_init (CamelSaslDigestMd5 *sasl) { sasl->priv = CAMEL_SASL_DIGEST_MD5_GET_PRIVATE (sasl); }