/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */ /* * Authors: Jeffrey Stedfast * * Copyright 2001 Ximian, Inc. (www.ximian.com) * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 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 General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 59 Temple Street #330, Boston, MA 02111-1307, USA. * */ #ifdef HAVE_CONFIG_H #include #endif #include #include #include #include #include #include "camel-sasl-digest-md5.h" #include "camel-mime-utils.h" #include "camel-charset-map.h" #include #define d(x) #define PARANOID(x) x CamelServiceAuthType camel_sasl_digest_md5_authtype = { 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 }; static CamelSaslClass *parent_class = NULL; /* Returns the class for a CamelSaslDigestMd5 */ #define CSCM_CLASS(so) CAMEL_SASL_DIGEST_MD5_CLASS (CAMEL_OBJECT_GET_CLASS (so)) static GByteArray *digest_md5_challenge (CamelSasl *sasl, GByteArray *token, CamelException *ex); enum { STATE_AUTH, STATE_FINAL }; typedef struct { char *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 { char *name; char *value; }; struct _DigestChallenge { GPtrArray *realms; char *nonce; guint qop; gboolean stale; gint32 maxbuf; char *charset; char *algorithm; guint cipher; GList *params; }; struct _DigestURI { char *type; char *host; char *name; }; struct _DigestResponse { char *username; char *realm; char *nonce; char *cnonce; char nc[9]; guint qop; struct _DigestURI *uri; char resp[33]; guint32 maxbuf; char *charset; guint cipher; char *authzid; char *param; }; struct _CamelSaslDigestMd5Private { struct _DigestChallenge *challenge; struct _DigestResponse *response; int state; }; static void camel_sasl_digest_md5_class_init (CamelSaslDigestMd5Class *camel_sasl_digest_md5_class) { CamelSaslClass *camel_sasl_class = CAMEL_SASL_CLASS (camel_sasl_digest_md5_class); parent_class = CAMEL_SASL_CLASS (camel_type_get_global_classfuncs (camel_sasl_get_type ())); /* virtual method overload */ camel_sasl_class->challenge = digest_md5_challenge; } static void camel_sasl_digest_md5_init (gpointer object, gpointer klass) { CamelSaslDigestMd5 *sasl_digest = CAMEL_SASL_DIGEST_MD5 (object); sasl_digest->priv = g_new0 (struct _CamelSaslDigestMd5Private, 1); } static void camel_sasl_digest_md5_finalize (CamelObject *object) { CamelSaslDigestMd5 *sasl = CAMEL_SASL_DIGEST_MD5 (object); struct _DigestChallenge *c = sasl->priv->challenge; struct _DigestResponse *r = sasl->priv->response; GList *p; int i; 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); 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); g_free (sasl->priv); } CamelType camel_sasl_digest_md5_get_type (void) { static CamelType type = CAMEL_INVALID_TYPE; if (type == CAMEL_INVALID_TYPE) { type = camel_type_register (camel_sasl_get_type (), "CamelSaslDigestMd5", sizeof (CamelSaslDigestMd5), sizeof (CamelSaslDigestMd5Class), (CamelObjectClassInitFunc) camel_sasl_digest_md5_class_init, NULL, (CamelObjectInitFunc) camel_sasl_digest_md5_init, (CamelObjectFinalizeFunc) camel_sasl_digest_md5_finalize); } return type; } static void decode_lwsp (const char **in) { const char *inptr = *in; while (isspace (*inptr)) inptr++; *in = inptr; } static char * decode_quoted_string (const char **in) { const char *inptr = *in; char *out = NULL, *outptr; int outlen; int c; decode_lwsp (&inptr); if (*inptr == '"') { const char *intmp; int 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 char * decode_token (const char **in) { const char *inptr = *in; const char *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 char * decode_value (const char **in) { const char *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 char *tokens) { GList *params = NULL; struct _param *param; const char *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 char *name) { int i; for (i = 0; dtype[i].name; i++) { if (!g_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 char * type_to_string (DataType *dtype, guint type) { int 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 char *tokens, gboolean *abort) { struct _DigestChallenge *challenge = NULL; GList *params, *p; const char *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; int type; type = get_digest_arg (param->name); switch (type) { case DIGEST_REALM: for (ptr = param->value; ptr && *ptr; ) { char *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; ) { char *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_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; ) { char *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 void digest_hex (guchar *digest, guchar hex[33]) { guchar *s, *p; /* lowercase hexify that bad-boy... */ for (s = digest, p = hex; p < hex + 32; s++, p += 2) sprintf (p, "%.2x", *s); } static char * 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 char *passwd, gboolean client, guchar out[33]) { guchar hex_a1[33], hex_a2[33]; guchar digest[16]; MD5Context ctx; char *buf; /* compute A1 */ md5_init (&ctx); md5_update (&ctx, resp->username, strlen (resp->username)); md5_update (&ctx, ":", 1); md5_update (&ctx, resp->realm, strlen (resp->realm)); md5_update (&ctx, ":", 1); md5_update (&ctx, passwd, strlen (passwd)); md5_final (&ctx, digest); md5_init (&ctx); md5_update (&ctx, digest, 16); md5_update (&ctx, ":", 1); md5_update (&ctx, resp->nonce, strlen (resp->nonce)); md5_update (&ctx, ":", 1); md5_update (&ctx, resp->cnonce, strlen (resp->cnonce)); if (resp->authzid) { md5_update (&ctx, ":", 1); md5_update (&ctx, resp->authzid, strlen (resp->authzid)); } /* hexify A1 */ md5_final (&ctx, digest); digest_hex (digest, hex_a1); /* compute A2 */ md5_init (&ctx); if (client) { /* we are calculating the client response */ md5_update (&ctx, "AUTHENTICATE:", strlen ("AUTHENTICATE:")); } else { /* we are calculating the server rspauth */ md5_update (&ctx, ":", 1); } buf = digest_uri_to_string (resp->uri); md5_update (&ctx, buf, strlen (buf)); g_free (buf); if (resp->qop == QOP_AUTH_INT || resp->qop == QOP_AUTH_CONF) md5_update (&ctx, ":00000000000000000000000000000000", 33); /* now hexify A2 */ md5_final (&ctx, digest); digest_hex (digest, hex_a2); /* compute KD */ md5_init (&ctx); md5_update (&ctx, hex_a1, 32); md5_update (&ctx, ":", 1); md5_update (&ctx, resp->nonce, strlen (resp->nonce)); md5_update (&ctx, ":", 1); md5_update (&ctx, resp->nc, 8); md5_update (&ctx, ":", 1); md5_update (&ctx, resp->cnonce, strlen (resp->cnonce)); md5_update (&ctx, ":", 1); md5_update (&ctx, qop_to_string (resp->qop), strlen (qop_to_string (resp->qop))); md5_update (&ctx, ":", 1); md5_update (&ctx, hex_a2, 32); md5_final (&ctx, digest); digest_hex (digest, out); } static struct _DigestResponse * generate_response (struct _DigestChallenge *challenge, struct hostent *host, const char *protocol, const char *user, const char *passwd) { struct _DigestResponse *resp; struct _DigestURI *uri; char *bgen, digest[16]; 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", resp, (unsigned long) getpid (), (unsigned long) time (0)); md5_get_digest (bgen, strlen (bgen), digest); g_free (bgen); /* take our recommended 64 bits of entropy */ resp->cnonce = base64_encode_simple (digest, 8); /* we don't support re-auth so the nonce count is always 1 */ strcpy (resp->nc, "00000001"); /* 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->h_name); 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, resp->resp); return resp; } static GByteArray * digest_response (struct _DigestResponse *resp) { GByteArray *buffer; const char *str; char *buf; buffer = g_byte_array_new (); g_byte_array_append (buffer, "username=\"", 10); if (resp->charset) { /* Encode the username using the requested charset */ char *charset, *username, *outbuf; size_t len, outlen; const char *buf; iconv_t cd; charset = camel_charset_locale_name (); if (!charset) charset = g_strdup ("iso-8859-1"); cd = iconv_open (resp->charset, charset); g_free (charset); len = strlen (resp->username); outlen = 2 * len; /* plenty of space */ outbuf = username = g_malloc0 (outlen + 1); buf = resp->username; if (cd == (iconv_t) -1 || iconv (cd, &buf, &len, &outbuf, &outlen) == -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) iconv_close (cd); g_byte_array_append (buffer, username, strlen (username)); g_free (username); } else { g_byte_array_append (buffer, resp->username, strlen (resp->username)); } g_byte_array_append (buffer, "\",realm=\"", 9); g_byte_array_append (buffer, resp->realm, strlen (resp->realm)); g_byte_array_append (buffer, "\",nonce=\"", 9); g_byte_array_append (buffer, resp->nonce, strlen (resp->nonce)); g_byte_array_append (buffer, "\",cnonce=\"", 10); g_byte_array_append (buffer, resp->cnonce, strlen (resp->cnonce)); g_byte_array_append (buffer, "\",nc=", 5); g_byte_array_append (buffer, resp->nc, 8); g_byte_array_append (buffer, ",qop=\"", 6); str = qop_to_string (resp->qop); g_byte_array_append (buffer, str, strlen (str)); g_byte_array_append (buffer, "\",digest-uri=\"", 14); buf = digest_uri_to_string (resp->uri); g_byte_array_append (buffer, buf, strlen (buf)); g_free (buf); g_byte_array_append (buffer, "\",response=\"", 12); g_byte_array_append (buffer, resp->resp, 32); g_byte_array_append (buffer, "\"", 1); if (resp->maxbuf > 0) { g_byte_array_append (buffer, ",maxbuf=", 8); buf = g_strdup_printf ("%d", resp->maxbuf); g_byte_array_append (buffer, buf, strlen (buf)); g_free (buf); } if (resp->charset) { g_byte_array_append (buffer, ",charset=\"", 10); g_byte_array_append (buffer, resp->charset, strlen (resp->charset)); g_byte_array_append (buffer, "\"", 1); } if (resp->cipher != CIPHER_INVALID) { str = cipher_to_string (resp->cipher); if (str) { g_byte_array_append (buffer, ",cipher=\"", 9); g_byte_array_append (buffer, str, strlen (str)); g_byte_array_append (buffer, "\"", 1); } } if (resp->authzid) { g_byte_array_append (buffer, ",authzid=\"", 10); g_byte_array_append (buffer, resp->authzid, strlen (resp->authzid)); g_byte_array_append (buffer, "\"", 1); } return buffer; } static GByteArray * digest_md5_challenge (CamelSasl *sasl, GByteArray *token, CamelException *ex) { CamelSaslDigestMd5 *sasl_digest = CAMEL_SASL_DIGEST_MD5 (sasl); struct _CamelSaslDigestMd5Private *priv = sasl_digest->priv; struct _param *rspauth; GByteArray *ret = NULL; gboolean abort = FALSE; struct hostent *h; const char *ptr; guchar out[33]; char *tokens; /* Need to wait for the server */ if (!token) return NULL; g_return_val_if_fail (sasl->service->url->passwd != NULL, NULL); switch (priv->state) { case STATE_AUTH: if (token->len > 2048) { camel_exception_setv (ex, CAMEL_EXCEPTION_SERVICE_CANT_AUTHENTICATE, _("Server challenge too long (>2048 octets)\n")); return NULL; } tokens = g_strndup (token->data, token->len); priv->challenge = parse_server_challenge (tokens, &abort); g_free (tokens); if (!priv->challenge || abort) { camel_exception_setv (ex, CAMEL_EXCEPTION_SERVICE_CANT_AUTHENTICATE, _("Server challenge invalid\n")); return NULL; } if (priv->challenge->qop == QOP_INVALID) { camel_exception_setv (ex, CAMEL_EXCEPTION_SERVICE_CANT_AUTHENTICATE, _("Server challenge contained invalid " "\"Quality of Protection\" token\n")); return NULL; } h = camel_service_gethost (sasl->service, ex); priv->response = generate_response (priv->challenge, h, sasl->service_name, sasl->service->url->user, sasl->service->url->passwd); camel_free_host(h); ret = digest_response (priv->response); break; case STATE_FINAL: if (token->len) tokens = g_strndup (token->data, token->len); else tokens = NULL; if (!tokens || !*tokens) { g_free (tokens); camel_exception_setv (ex, CAMEL_EXCEPTION_SERVICE_CANT_AUTHENTICATE, _("Server response did not contain authorization data\n")); return NULL; } 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); camel_exception_setv (ex, CAMEL_EXCEPTION_SERVICE_CANT_AUTHENTICATE, _("Server response contained incomplete authorization data\n")); return NULL; } compute_response (priv->response, sasl->service->url->passwd, FALSE, out); if (memcmp (out, rspauth->value, 32) != 0) { g_free (rspauth->name); g_free (rspauth->value); g_free (rspauth); camel_exception_setv (ex, CAMEL_EXCEPTION_SERVICE_CANT_AUTHENTICATE, _("Server response does not match\n")); sasl->authenticated = TRUE; return NULL; } g_free (rspauth->name); g_free (rspauth->value); g_free (rspauth); ret = g_byte_array_new (); sasl->authenticated = TRUE; default: break; } priv->state++; return ret; }