aboutsummaryrefslogblamecommitdiffstats
path: root/camel/camel-sasl-digest-md5.c
blob: 51b2026abc697797c1f5a567c77926211fc4cb11 (plain) (tree)





















                                                                           
                    
                   

      



                   
                  

                                  
                              

                             
 










                                                                     
            















































































































































































































































                                                                                                        
                               




































































































































































































                                                                                   



                                                                  















































                                                                                      












                                                                                                    
                          
                       


                        

















                                                                         
        

                                 


                                    
                        
                     

                                                                             

                                                           
                                          

         



                                               
                                                                    



                                                                           


                                    













                                                                                         
                                 












                                                                              
                                                            






























                                                                     




                                                                 



                                         
                                             



















                                                          






                                                                     
                                                       
                             
                                                          

                                                         
                                 






                                                                                           




                                                                                               







                                                                          
                                  


                                                                                      







































































































                                                                                               
                                   




























































                                                                                                              
/* -*- Mode: C; tab-width: 8; indent-tabs-mode: t; c-basic-offset: 8 -*- */
/*
 *  Authors: Jeffrey Stedfast <fejj@ximian.com>
 *
 *  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 <config.h>
#endif

#include <stdio.h>
#include <string.h>
#include <ctype.h>
#include <unistd.h>
#include <iconv.h>
#include "camel-sasl-digest-md5.h"
#include "camel-mime-utils.h"
#include "camel-charset-map.h"
#include <e-util/md5-utils.h>


#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;
}