Coverage Report

Created: 2026-02-14 07:05

next uncovered line (L), next uncovered region (R), next uncovered branch (B)
/src/pidgin/libpurple/protocols/jabber/auth_scram.c
Line
Count
Source
1
/*
2
 * purple - Jabber Protocol Plugin
3
 *
4
 * Purple is the legal property of its developers, whose names are too numerous
5
 * to list here.  Please refer to the COPYRIGHT file distributed with this
6
 * source distribution.
7
 *
8
 * This program is free software; you can redistribute it and/or modify
9
 * it under the terms of the GNU General Public License as published by
10
 * the Free Software Foundation; either version 2 of the License, or
11
 * (at your option) any later version.
12
 *
13
 * This program is distributed in the hope that it will be useful,
14
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
15
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16
 * GNU General Public License for more details.
17
 *
18
 * You should have received a copy of the GNU General Public License
19
 * along with this program; if not, write to the Free Software
20
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02111-1301  USA
21
 *
22
 */
23
#include "internal.h"
24
25
#include "auth.h"
26
#include "auth_scram.h"
27
28
#include "cipher.h"
29
#include "debug.h"
30
31
static const JabberScramHash hashes[] = {
32
  { "-SHA-1", "sha1", 20 },
33
};
34
35
static const JabberScramHash *mech_to_hash(const char *mech)
36
0
{
37
0
  gsize i;
38
39
0
  g_return_val_if_fail(mech != NULL && *mech != '\0', NULL);
40
41
0
  for (i = 0; i < G_N_ELEMENTS(hashes); ++i) {
42
0
    if (strstr(mech, hashes[i].mech_substr))
43
0
      return &(hashes[i]);
44
0
  }
45
46
0
  purple_debug_error("jabber", "Unknown SCRAM mechanism %s\n", mech);
47
0
  g_return_val_if_reached(NULL);
48
0
}
49
50
guchar *jabber_scram_hi(const JabberScramHash *hash, const GString *str,
51
                        GString *salt, guint iterations)
52
0
{
53
0
  PurpleCipherContext *context;
54
0
  guchar *result;
55
0
  guint i;
56
0
  guchar *prev, *tmp;
57
58
0
  g_return_val_if_fail(hash != NULL, NULL);
59
0
  g_return_val_if_fail(str != NULL && str->len > 0, NULL);
60
0
  g_return_val_if_fail(salt != NULL && salt->len > 0, NULL);
61
0
  g_return_val_if_fail(iterations > 0, NULL);
62
63
0
  prev   = g_new0(guint8, hash->size);
64
0
  tmp    = g_new0(guint8, hash->size);
65
0
  result = g_new0(guint8, hash->size);
66
67
0
  context = purple_cipher_context_new_by_name("hmac", NULL);
68
69
  /* Append INT(1), a four-octet encoding of the integer 1, most significant
70
   * octet first. */
71
0
  g_string_append_len(salt, "\0\0\0\1", 4);
72
73
  /* Compute U0 */
74
0
  purple_cipher_context_set_option(context, "hash", (gpointer)hash->name);
75
0
  purple_cipher_context_set_key_with_len(context, (guchar *)str->str, str->len);
76
0
  purple_cipher_context_append(context, (guchar *)salt->str, salt->len);
77
0
  purple_cipher_context_digest(context, hash->size, result, NULL);
78
79
0
  memcpy(prev, result, hash->size);
80
81
  /* Compute U1...Ui */
82
0
  for (i = 1; i < iterations; ++i) {
83
0
    guint j;
84
0
    purple_cipher_context_set_option(context, "hash", (gpointer)hash->name);
85
0
    purple_cipher_context_set_key_with_len(context, (guchar *)str->str, str->len);
86
0
    purple_cipher_context_append(context, prev, hash->size);
87
0
    purple_cipher_context_digest(context, hash->size, tmp, NULL);
88
89
0
    for (j = 0; j < hash->size; ++j)
90
0
      result[j] ^= tmp[j];
91
92
0
    memcpy(prev, tmp, hash->size);
93
0
  }
94
95
0
  purple_cipher_context_destroy(context);
96
0
  g_free(tmp);
97
0
  g_free(prev);
98
0
  return result;
99
0
}
100
101
/*
102
 * Helper functions for doing the SCRAM calculations. The first argument
103
 * is the hash algorithm.  All buffers must be of the appropriate size
104
 * according to the JabberScramHash.
105
 *
106
 * "str" is a NULL-terminated string for jabber_scram_hmac().
107
 *
108
 * Needless to say, these are fragile.
109
 */
110
static void
111
jabber_scram_hmac(const JabberScramHash *hash, guchar *out, const guchar *key, const gchar *str)
112
0
{
113
0
  PurpleCipherContext *context;
114
115
0
  context = purple_cipher_context_new_by_name("hmac", NULL);
116
0
  purple_cipher_context_set_option(context, "hash", (gpointer)hash->name);
117
0
  purple_cipher_context_set_key_with_len(context, key, hash->size);
118
0
  purple_cipher_context_append(context, (guchar *)str, strlen(str));
119
0
  purple_cipher_context_digest(context, hash->size, out, NULL);
120
0
  purple_cipher_context_destroy(context);
121
0
}
122
123
static void
124
jabber_scram_hash(const JabberScramHash *hash, guchar *out, const guchar *data)
125
0
{
126
0
  PurpleCipherContext *context;
127
128
0
  context = purple_cipher_context_new_by_name(hash->name, NULL);
129
0
  purple_cipher_context_append(context, data, hash->size);
130
0
  purple_cipher_context_digest(context, hash->size, out, NULL);
131
0
  purple_cipher_context_destroy(context);
132
0
}
133
134
gboolean
135
jabber_scram_calc_proofs(JabberScramData *data, GString *salt, guint iterations)
136
0
{
137
0
  guint hash_len = data->hash->size;
138
0
  guint i;
139
140
0
  GString *pass = g_string_new(data->password);
141
142
0
  guchar *salted_password;
143
0
  guchar *client_key, *stored_key, *client_signature, *server_key;
144
145
0
  salted_password = jabber_scram_hi(data->hash, pass, salt, iterations);
146
0
  memset(pass->str, 0, pass->allocated_len);
147
0
  g_string_free(pass, TRUE);
148
149
0
  if (!salted_password)
150
0
    return FALSE;
151
152
0
  client_key = g_new0(guchar, hash_len);
153
0
  stored_key = g_new0(guchar, hash_len);
154
0
  client_signature = g_new0(guchar, hash_len);
155
0
  server_key = g_new0(guchar, hash_len);
156
157
0
  data->client_proof = g_string_sized_new(hash_len);
158
0
  data->client_proof->len = hash_len;
159
0
  data->server_signature = g_string_sized_new(hash_len);
160
0
  data->server_signature->len = hash_len;
161
162
  /* client_key = HMAC(salted_password, "Client Key") */
163
0
  jabber_scram_hmac(data->hash, client_key, salted_password, "Client Key");
164
  /* server_key = HMAC(salted_password, "Server Key") */
165
0
  jabber_scram_hmac(data->hash, server_key, salted_password, "Server Key");
166
0
  g_free(salted_password);
167
168
  /* stored_key = HASH(client_key) */
169
0
  jabber_scram_hash(data->hash, stored_key, client_key);
170
171
  /* client_signature = HMAC(stored_key, auth_message) */
172
0
  jabber_scram_hmac(data->hash, client_signature, stored_key, data->auth_message->str);
173
  /* server_signature = HMAC(server_key, auth_message) */
174
0
  jabber_scram_hmac(data->hash, (guchar *)data->server_signature->str, server_key, data->auth_message->str);
175
176
  /* client_proof = client_key XOR client_signature */
177
0
  for (i = 0; i < hash_len; ++i)
178
0
    data->client_proof->str[i] = client_key[i] ^ client_signature[i];
179
180
0
  g_free(server_key);
181
0
  g_free(client_signature);
182
0
  g_free(stored_key);
183
0
  g_free(client_key);
184
185
0
  return TRUE;
186
0
}
187
188
static gboolean
189
parse_server_step1(JabberScramData *data, const char *challenge,
190
                   gchar **out_nonce, GString **out_salt, guint *out_iterations)
191
0
{
192
0
  char **tokens;
193
0
  char *token, *decoded, *tmp;
194
0
  gsize len;
195
0
  char *nonce = NULL;
196
0
  GString *salt = NULL;
197
0
  guint iterations;
198
199
0
  tokens = g_strsplit(challenge, ",", -1);
200
0
  if (tokens == NULL)
201
0
    return FALSE;
202
203
0
  token = tokens[0];
204
0
  if (token[0] != 'r' || token[1] != '=')
205
0
    goto err;
206
207
  /* Ensure that the first cnonce_len bytes of the nonce are the original
208
   * cnonce we sent to the server.
209
   */
210
0
  if (0 != strncmp(data->cnonce, token + 2, strlen(data->cnonce)))
211
0
    goto err;
212
213
0
  nonce = g_strdup(token + 2);
214
215
  /* The Salt, base64-encoded */
216
0
  token = tokens[1];
217
0
  if (token[0] != 's' || token[1] != '=')
218
0
    goto err;
219
220
0
  decoded = (gchar *)purple_base64_decode(token + 2, &len);
221
0
  if (!decoded || *decoded == '\0') {
222
0
    g_free(decoded);
223
0
    goto err;
224
0
  }
225
0
  salt = g_string_new_len(decoded, len);
226
0
  g_free(decoded);
227
228
  /* The iteration count */
229
0
  token = tokens[2];
230
0
  if (token[0] != 'i' || token[1] != '=' || token[2] == '\0')
231
0
    goto err;
232
233
  /* Validate the string */
234
0
  for (tmp = token + 2; *tmp; ++tmp)
235
0
    if (!g_ascii_isdigit(*tmp))
236
0
      goto err;
237
238
0
  iterations = strtoul(token + 2, NULL, 10);
239
240
0
  g_strfreev(tokens);
241
0
  *out_nonce = nonce;
242
0
  *out_salt = salt;
243
0
  *out_iterations = iterations;
244
0
  return TRUE;
245
246
0
err:
247
0
  g_free(nonce);
248
0
  if (salt)
249
0
    g_string_free(salt, TRUE);
250
0
  g_strfreev(tokens);
251
0
  return FALSE;
252
0
}
253
254
static gboolean
255
parse_server_step2(JabberScramData *data, const char *challenge, gchar **out_verifier)
256
0
{
257
0
  char **tokens;
258
0
  char *token;
259
260
0
  tokens = g_strsplit(challenge, ",", -1);
261
0
  if (tokens == NULL)
262
0
    return FALSE;
263
264
0
  token = tokens[0];
265
0
  if (token[0] != 'v' || token[1] != '=' || token[2] == '\0') {
266
0
    g_strfreev(tokens);
267
0
    return FALSE;
268
0
  }
269
270
0
  *out_verifier = g_strdup(token + 2);
271
0
  g_strfreev(tokens);
272
0
  return TRUE;
273
0
}
274
275
gboolean
276
jabber_scram_feed_parser(JabberScramData *data, gchar *in, gchar **out)
277
0
{
278
0
  gboolean ret;
279
280
0
  g_return_val_if_fail(data != NULL, FALSE);
281
282
0
  g_string_append_c(data->auth_message, ',');
283
0
  g_string_append(data->auth_message, in);
284
285
0
  if (data->step == 1) {
286
0
    gchar *nonce, *proof;
287
0
    GString *salt;
288
0
    guint iterations;
289
290
0
    ret = parse_server_step1(data, in, &nonce, &salt, &iterations);
291
0
    if (!ret)
292
0
      return FALSE;
293
294
0
    g_string_append_c(data->auth_message, ',');
295
296
    /* "biws" is the base64 encoding of "n,,". I promise. */
297
0
    g_string_append_printf(data->auth_message, "c=%s,r=%s", "biws", nonce);
298
#ifdef CHANNEL_BINDING
299
#error fix this
300
#endif
301
302
0
    ret = jabber_scram_calc_proofs(data, salt, iterations);
303
304
0
    g_string_free(salt, TRUE);
305
0
    salt = NULL;
306
0
    if (!ret) {
307
0
      g_free(nonce);
308
0
      return FALSE;
309
0
    }
310
311
0
    proof = purple_base64_encode((guchar *)data->client_proof->str, data->client_proof->len);
312
0
    *out = g_strdup_printf("c=%s,r=%s,p=%s", "biws", nonce, proof);
313
0
    g_free(nonce);
314
0
    g_free(proof);
315
0
  } else if (data->step == 2) {
316
0
    gchar *server_sig, *enc_server_sig;
317
0
    gsize len;
318
319
0
    ret = parse_server_step2(data, in, &enc_server_sig);
320
0
    if (!ret)
321
0
      return FALSE;
322
323
0
    server_sig = (gchar *)purple_base64_decode(enc_server_sig, &len);
324
0
    g_free(enc_server_sig);
325
326
0
    if (server_sig == NULL || len != data->server_signature->len) {
327
0
      g_free(server_sig);
328
0
      return FALSE;
329
0
    }
330
331
0
    if (0 != memcmp(server_sig, data->server_signature->str, len)) {
332
0
      g_free(server_sig);
333
0
      return FALSE;
334
0
    }
335
0
    g_free(server_sig);
336
337
0
    *out = NULL;
338
0
  } else {
339
0
    purple_debug_error("jabber", "SCRAM: There is no step %d\n", data->step);
340
0
    return FALSE;
341
0
  }
342
343
0
  return TRUE;
344
0
}
345
346
static gchar *escape_username(const gchar *in)
347
0
{
348
0
  gchar *tmp, *tmp2;
349
350
0
  tmp = purple_strreplace(in, "=", "=3D");
351
0
  tmp2 = purple_strreplace(tmp, ",", "=2C");
352
0
  g_free(tmp);
353
0
  return tmp2;
354
0
}
355
356
static JabberSaslState
357
scram_start(JabberStream *js, xmlnode *mechanisms, xmlnode **out, char **error)
358
0
{
359
0
  xmlnode *reply;
360
0
  JabberScramData *data;
361
0
  guint64 cnonce;
362
#ifdef CHANNEL_BINDING
363
  gboolean binding_supported = TRUE;
364
#endif
365
0
  gchar *dec_out, *enc_out;
366
0
  gchar *prepped_node, *tmp;
367
0
  gchar *prepped_pass;
368
369
0
  prepped_node = jabber_saslprep(js->user->node);
370
0
  if (!prepped_node) {
371
0
    *error = g_strdup(_("Unable to canonicalize username"));
372
0
    return JABBER_SASL_STATE_FAIL;
373
0
  }
374
375
0
  tmp = escape_username(prepped_node);
376
0
  g_free(prepped_node);
377
0
  prepped_node = tmp;
378
379
0
  prepped_pass = jabber_saslprep(purple_connection_get_password(js->gc));
380
0
  if (!prepped_pass) {
381
0
    g_free(prepped_node);
382
0
    *error = g_strdup(_("Unable to canonicalize password"));
383
0
    return JABBER_SASL_STATE_FAIL;
384
0
  }
385
386
0
  data = js->auth_mech_data = g_new0(JabberScramData, 1);
387
0
  data->hash = mech_to_hash(js->auth_mech->name);
388
0
  data->password = prepped_pass;
389
390
#ifdef CHANNEL_BINDING
391
  if (strstr(js->auth_mech_name, "-PLUS"))
392
    data->channel_binding = TRUE;
393
#endif
394
0
  cnonce = ((guint64)g_random_int() << 32) | g_random_int();
395
0
  data->cnonce = purple_base64_encode((guchar *)&cnonce, sizeof(cnonce));
396
397
0
  data->auth_message = g_string_new(NULL);
398
0
  g_string_printf(data->auth_message, "n=%s,r=%s",
399
0
      prepped_node, data->cnonce);
400
0
  g_free(prepped_node);
401
402
0
  data->step = 1;
403
404
0
  reply = xmlnode_new("auth");
405
0
  xmlnode_set_namespace(reply, NS_XMPP_SASL);
406
0
  xmlnode_set_attrib(reply, "mechanism", js->auth_mech->name);
407
408
  /* TODO: Channel binding */
409
0
  dec_out = g_strdup_printf("%c,,%s", 'n', data->auth_message->str);
410
0
  enc_out = purple_base64_encode((guchar *)dec_out, strlen(dec_out));
411
0
  purple_debug_misc("jabber", "initial SCRAM message '%s'\n", dec_out);
412
413
0
  xmlnode_insert_data(reply, enc_out, -1);
414
415
0
  g_free(enc_out);
416
0
  g_free(dec_out);
417
418
0
  *out = reply;
419
0
  return JABBER_SASL_STATE_CONTINUE;
420
0
}
421
422
static JabberSaslState
423
scram_handle_challenge(JabberStream *js, xmlnode *challenge, xmlnode **out, char **error)
424
0
{
425
0
  JabberScramData *data = js->auth_mech_data;
426
0
  xmlnode *reply;
427
0
  gchar *enc_in, *dec_in = NULL;
428
0
  gchar *enc_out = NULL, *dec_out = NULL;
429
0
  gsize len;
430
0
  JabberSaslState state = JABBER_SASL_STATE_FAIL;
431
432
0
  enc_in = xmlnode_get_data(challenge);
433
0
  if (!enc_in || *enc_in == '\0') {
434
0
    reply = xmlnode_new("abort");
435
0
    xmlnode_set_namespace(reply, NS_XMPP_SASL);
436
0
    data->step = -1;
437
0
    *error = g_strdup(_("Invalid challenge from server"));
438
0
    goto out;
439
0
  }
440
441
0
  dec_in = (gchar *)purple_base64_decode(enc_in, &len);
442
0
  if (!dec_in || len != strlen(dec_in)) {
443
    /* Danger afoot; SCRAM shouldn't contain NUL bytes */
444
0
    reply = xmlnode_new("abort");
445
0
    xmlnode_set_namespace(reply, NS_XMPP_SASL);
446
0
    data->step = -1;
447
0
    *error = g_strdup(_("Malicious challenge from server"));
448
0
    goto out;
449
0
  }
450
451
0
  purple_debug_misc("jabber", "decoded challenge: %s\n", dec_in);
452
453
0
  if (!jabber_scram_feed_parser(data, dec_in, &dec_out)) {
454
0
    reply = xmlnode_new("abort");
455
0
    xmlnode_set_namespace(reply, NS_XMPP_SASL);
456
0
    data->step = -1;
457
0
    *error = g_strdup(_("Invalid challenge from server"));
458
0
    goto out;
459
0
  }
460
461
0
  data->step += 1;
462
463
0
  reply = xmlnode_new("response");
464
0
  xmlnode_set_namespace(reply, NS_XMPP_SASL);
465
466
0
  purple_debug_misc("jabber", "decoded response: %s\n", dec_out ? dec_out : "(null)");
467
0
  if (dec_out) {
468
0
    enc_out = purple_base64_encode((guchar *)dec_out, strlen(dec_out));
469
0
    xmlnode_insert_data(reply, enc_out, -1);
470
0
  }
471
472
0
  state = JABBER_SASL_STATE_CONTINUE;
473
474
0
out:
475
0
  g_free(enc_in);
476
0
  g_free(dec_in);
477
0
  g_free(enc_out);
478
0
  g_free(dec_out);
479
480
0
  *out = reply;
481
0
  return state;
482
0
}
483
484
static JabberSaslState
485
scram_handle_success(JabberStream *js, xmlnode *packet, char **error)
486
0
{
487
0
  JabberScramData *data = js->auth_mech_data;
488
0
  char *enc_in, *dec_in;
489
0
  char *dec_out = NULL;
490
0
  gsize len;
491
492
0
  enc_in = xmlnode_get_data(packet);
493
0
  if (data->step != 3 && (!enc_in || *enc_in == '\0')) {
494
0
    *error = g_strdup(_("Invalid challenge from server"));
495
0
    g_free(enc_in);
496
0
    return JABBER_SASL_STATE_FAIL;
497
0
  }
498
499
0
  if (data->step == 3) {
500
    /*
501
     * If the server took the slow approach (sending the verifier
502
     * as a challenge/response pair), we get here.
503
     */
504
0
    g_free(enc_in);
505
0
    return JABBER_SASL_STATE_OK;
506
0
  }
507
508
0
  if (data->step != 2) {
509
0
    *error = g_strdup(_("Unexpected response from server"));
510
0
    g_free(enc_in);
511
0
    return JABBER_SASL_STATE_FAIL;
512
0
  }
513
514
0
  dec_in = (gchar *)purple_base64_decode(enc_in, &len);
515
0
  g_free(enc_in);
516
0
  if (!dec_in || len != strlen(dec_in)) {
517
    /* Danger afoot; SCRAM shouldn't contain NUL bytes */
518
0
    g_free(dec_in);
519
0
    *error = g_strdup(_("Malicious challenge from server"));
520
0
    return JABBER_SASL_STATE_FAIL;
521
0
  }
522
523
0
  purple_debug_misc("jabber", "decoded success: %s\n", dec_in);
524
525
0
  if (!jabber_scram_feed_parser(data, dec_in, &dec_out) || dec_out != NULL) {
526
0
    g_free(dec_in);
527
0
    g_free(dec_out);
528
0
    *error = g_strdup(_("Invalid challenge from server"));
529
0
    return JABBER_SASL_STATE_FAIL;
530
0
  }
531
532
0
  g_free(dec_in);
533
  /* Hooray */
534
0
  return JABBER_SASL_STATE_OK;
535
0
}
536
537
void jabber_scram_data_destroy(JabberScramData *data)
538
0
{
539
0
  g_free(data->cnonce);
540
0
  if (data->auth_message)
541
0
    g_string_free(data->auth_message, TRUE);
542
0
  if (data->client_proof)
543
0
    g_string_free(data->client_proof, TRUE);
544
0
  if (data->server_signature)
545
0
    g_string_free(data->server_signature, TRUE);
546
0
  if (data->password) {
547
0
    memset(data->password, 0, strlen(data->password));
548
0
    g_free(data->password);
549
0
  }
550
551
0
  g_free(data);
552
0
}
553
554
static void scram_dispose(JabberStream *js)
555
0
{
556
0
  if (js->auth_mech_data) {
557
0
    jabber_scram_data_destroy(js->auth_mech_data);
558
0
    js->auth_mech_data = NULL;
559
0
  }
560
0
}
561
562
static JabberSaslMech scram_sha1_mech = {
563
  50, /* priority */
564
  "SCRAM-SHA-1", /* name */
565
  scram_start,
566
  scram_handle_challenge,
567
  scram_handle_success,
568
  NULL, /* handle_failure */
569
  scram_dispose
570
};
571
572
#ifdef CHANNEL_BINDING
573
/* With channel binding */
574
static JabberSaslMech scram_sha1_plus_mech = {
575
  scram_sha1_mech.priority + 1, /* priority */
576
  "SCRAM-SHA-1-PLUS", /* name */
577
  scram_start,
578
  scram_handle_challenge,
579
  scram_handle_success,
580
  NULL, /* handle_failure */
581
  scram_dispose
582
};
583
#endif
584
585
JabberSaslMech **jabber_auth_get_scram_mechs(gint *count)
586
0
{
587
0
  static JabberSaslMech *mechs[] = {
588
0
    &scram_sha1_mech,
589
#ifdef CHANNEL_BINDING
590
    &scram_sha1_plus_mech,
591
#endif
592
0
  };
593
594
0
  *count = G_N_ELEMENTS(mechs);
595
0
  return mechs;
596
0
}