[calls] sip: Add srtp-utilities



commit 493772354172d418ae8d79118090e75cd8c1be80
Author: Evangelos Ribeiro Tzaras <devrtz fortysixandtwo eu>
Date:   Thu Apr 14 17:09:57 2022 +0200

    sip: Add srtp-utilities
    
    These utilities aid in generating and parsing SDP crypto attributes to be used
    during the offer/answer negotiation.

 plugins/sip/calls-srtp-utils.c | 693 +++++++++++++++++++++++++++++++++++++++++
 plugins/sip/calls-srtp-utils.h | 106 +++++++
 plugins/sip/meson.build        |   1 +
 tests/meson.build              |  15 +
 tests/test-srtp.c              | 236 ++++++++++++++
 5 files changed, 1051 insertions(+)
---
diff --git a/plugins/sip/calls-srtp-utils.c b/plugins/sip/calls-srtp-utils.c
new file mode 100644
index 00000000..a7a87e65
--- /dev/null
+++ b/plugins/sip/calls-srtp-utils.c
@@ -0,0 +1,693 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * This file is part of Calls.
+ *
+ * Calls 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Calls 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 Calls.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Evangelos Ribeiro Tzaras <evangelos tzaras puri sm>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#include "calls-srtp-utils.h"
+#include "util.h"
+
+#include <gio/gio.h>
+#include <sys/random.h>
+
+/**
+ * SECTION:srtp-utils
+ * @short_description: SRTP utilities for SDP parsing
+ * @Title: CallsSrtpUtils
+ *
+ * Utilities for parsing and generating the crypto attribute
+ * in SDP for SRTP use based on RFC 4568.
+ *
+ * Note that limitations of libsrtp are taken into account when checking validity
+ * of the parsed attribute. These are:
+ * A maximum of 16 keys,
+ * key derivation rate must be 0,
+ * lifetimes other than 2^48 (we actually ignore the specified lifetimes)
+ */
+
+
+/* The default used in libsrtp. No API to change this. See https://github.com/cisco/libsrtp/issues/588 */
+#define SRTP_DEFAULT_LIFETIME_POW2 48
+#define SRTP_MAX_LIFETIME_POW2 48
+
+/* The default used in libsrtp (and GstSrtpEnc/GstSrtpDec) */
+#define SRTP_DEFAULT_WINDOW_SIZE 128
+
+const char * srtp_crypto_suites[] = {
+  "AES_CM_128_HMAC_SHA1_32", /* RFC 4568 */
+  "AES_CM_128_HMAC_SHA1_80", /* RFC 4568 */
+  "F8_128_HMAC_SHA1_32", /* RFC 4568 but not supported by GstSrtpEnc/GstSrtpDec */
+  "AEAD_AES_128_GCM", /* RFC 7714 TODO support in the future */
+  "AEAD_AES_256_GCM", /* RFC 7714 TODO support in the future */
+  NULL
+};
+
+
+static gsize
+get_key_size_for_suite (calls_srtp_crypto_suite suite)
+{
+  switch (suite) {
+  case CALLS_SRTP_SUITE_AES_128_SHA1_32:
+  case CALLS_SRTP_SUITE_AES_128_SHA1_80:
+    return 30;
+
+  case CALLS_SRTP_SUITE_UNKNOWN:
+  default:
+    return 0;
+  }
+}
+
+
+static gboolean
+validate_crypto_attribute (calls_srtp_crypto_attribute *attr,
+                           GError                     **error)
+{
+  guint expected_key_salt_length;
+  gboolean need_mki;
+  guint expected_mki_length = 0;
+  calls_srtp_crypto_key_param *key_param;
+  GSList *mki_list = NULL; /* for checking uniqueness of MKIs */
+  GSList *key_list = NULL; /* for checking uniqueness of keys */
+
+  if (!attr) {
+    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                 "Attribute is NULL");
+    return FALSE;
+  }
+
+  if (attr->tag <= 0 || attr->tag >= 1000000000) {
+    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                 "Tag is not valid: %d", attr->tag);
+    return FALSE;
+  }
+
+  switch (attr->crypto_suite) {
+  case CALLS_SRTP_SUITE_AES_128_SHA1_32:
+  case CALLS_SRTP_SUITE_AES_128_SHA1_80:
+    expected_key_salt_length = 30; /* 16 byte key + 14 byte salt */
+    break;
+
+  case CALLS_SRTP_SUITE_UNKNOWN:
+  default:
+    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                 "Crypto suite unknown");
+    return FALSE;
+  }
+
+  /* at least one and a maximum of 16 key parameters */
+  if (attr->n_key_params == 0 || attr->n_key_params > 16) {
+    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                 "Must have between 1 and 16 keys, got %d", attr->n_key_params);
+    return FALSE;
+  }
+
+  need_mki = attr->n_key_params > 1 ||
+             attr->key_params[0].mki ||
+             attr->key_params[0].mki_length;
+
+  if (need_mki) {
+    expected_mki_length = attr->key_params[0].mki_length;
+    if (expected_mki_length == 0 ||
+        expected_mki_length > 128) {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "MKI length must be between 1 and 128, got %u",
+                   expected_key_salt_length);
+      return FALSE;
+    }
+  }
+
+  for (guint i = 0; i < attr->n_key_params; i++) {
+    g_autofree guchar *key_salt = NULL;
+    gsize key_salt_length;
+
+    key_param = &attr->key_params[i];
+
+    /* must have a key */
+    if (!key_param->b64_keysalt) {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "No key found in parameter %d", i);
+      goto failed;
+    }
+
+    key_salt = g_base64_decode (key_param->b64_keysalt, &key_salt_length);
+
+    /* key must have length consistent with suite */
+    if (key_salt_length != expected_key_salt_length) {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Key %d has length %" G_GSIZE_FORMAT ", but expected %d",
+                   i, key_salt_length, expected_key_salt_length);
+      goto failed;
+    }
+
+    /* key must be unique */
+    if (g_slist_find_custom (key_list,
+                             key_param->b64_keysalt,
+                             (GCompareFunc) g_strcmp0)) {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Key %d is not unique: %s", i, key_param->b64_keysalt);
+      goto failed;
+    }
+
+    key_list = g_slist_append (key_list, key_param->b64_keysalt);
+
+    /* lifetime in range */
+    if (key_param->lifetime_type == CALLS_SRTP_LIFETIME_AS_DECIMAL_NUMBER &&
+        key_param->lifetime >= (1ULL << SRTP_MAX_LIFETIME_POW2)) {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Lifetime of key %d out of bounds: Got %" G_GINT64_FORMAT " but maximum is 2^%d",
+                   i, key_param->lifetime, SRTP_MAX_LIFETIME_POW2);
+      goto failed;
+    }
+
+    if (key_param->lifetime_type == CALLS_SRTP_LIFETIME_AS_POWER_OF_TWO &&
+        key_param->lifetime >= SRTP_MAX_LIFETIME_POW2) {
+      g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                   "Lifetime of key %d out of bounds: Got 2^%" G_GINT64_FORMAT " but maximum is 2^%d",
+                   i, key_param->lifetime, SRTP_MAX_LIFETIME_POW2);
+      goto failed;
+    }
+
+    /* if MKI length is set, it must be the same for all key parameters */
+    if (need_mki) {
+      if (key_param->mki_length != expected_mki_length) {
+        g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                     "MKI length must be the same for all keys. Key %d has length %d but expected %d",
+                     i, key_param->mki_length, expected_mki_length);
+        goto failed;
+      }
+
+      /* MKI must not have leading zero */
+      if (key_param->mki == 0) {
+        g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                     "No MKI set for key %d", i);
+        goto failed;
+      }
+
+      /* MKI must be unique */
+      if (g_slist_find (mki_list, GINT_TO_POINTER (key_param->mki))) {
+        g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                     "MKI for key %d is not unique", i);
+        goto failed;
+      }
+
+      mki_list = g_slist_append (mki_list, GINT_TO_POINTER (key_param->mki));
+    }
+
+  }
+
+  /* check session parameters */
+
+  /* libsrtp does only support kdr=0 */
+  if (attr->kdr != 0) {
+    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                 "Key derivation rate must be 0, got %d",
+                 attr->kdr);
+    goto failed;
+  }
+
+  g_slist_free (mki_list);
+  g_slist_free (key_list);
+
+  return TRUE;
+
+failed:
+
+  g_slist_free (mki_list);
+  g_slist_free (key_list);
+
+  return FALSE;
+}
+
+/**
+ * calls_srtp_generate_key_salt:
+ * @length: Desired length of random data
+ *
+ * Generate random data to be used as master key and master salt of desired length @length
+ *
+ * Returns: (transfer full): Random data to be used as key and salt in SRTP
+ * or %NULL if failed. Free with g_free() when done.
+ */
+guchar *
+calls_srtp_generate_key_salt (gsize length)
+{
+  g_autofree guchar *key_salt = NULL;
+  gsize n_bytes;
+
+  g_return_val_if_fail (length > 0, NULL);
+
+  key_salt = g_malloc (length);
+
+  n_bytes = getrandom (key_salt, length, GRND_NONBLOCK);
+  if (n_bytes == -1) {
+    return NULL;
+  }
+
+  return g_steal_pointer (&key_salt);
+}
+
+
+/**
+ * calls_srtp_generate_key_salt_for_suite:
+ * @suite: a #calls_srtp_crypto_suite
+ *
+ * Generate random data to be used as master key and master salt.
+ * The required length is determined by the requirements of the @suite
+ *
+ * Returns: (transfer full): Random data to be used as key and salt in SRTP
+ * or %NULL if failed. Free with g_free() when done.
+ */
+guchar *
+calls_srtp_generate_key_salt_for_suite (calls_srtp_crypto_suite suite)
+{
+  gsize size = get_key_size_for_suite (suite);
+
+  if (size == 0)
+    return NULL;
+
+  return calls_srtp_generate_key_salt (size);
+}
+
+/**
+ * calls_srtp_parse_sdp_crypto_attribute:
+ * @attribute: attribute line
+ * @error: a #GError
+ *
+ * Parse textual attribute line into structured data.
+ *
+ * Returns: (transfer full): A #calls_srtp_crypto_attribute containing
+ * parsed attribute data, or %NULL if parsing failed.
+ */
+calls_srtp_crypto_attribute *
+calls_srtp_parse_sdp_crypto_attribute (const char *attribute,
+                                       GError    **error)
+{
+  g_auto (GStrv) attr_fields = NULL;
+  g_auto (GStrv) key_info_strv = NULL;
+  guint n_attr_fields;
+  guint n_key_params;
+  char *tag_str;
+  gint tag;
+  calls_srtp_crypto_attribute *attr;
+  calls_srtp_crypto_suite crypto_suite;
+  gboolean need_mki;
+  gboolean attr_invalid = FALSE;
+  g_autofree char *err_msg = NULL;
+
+  if (STR_IS_NULL_OR_EMPTY (attribute)) {
+    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                 "Cannot parse null or empty strings");
+    return NULL;
+  }
+
+  if (!g_str_has_prefix (attribute, "a=crypto:")) {
+    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                 "Does not look like a SDP crypto attribute: %s",
+                 attribute);
+
+    return NULL;
+  }
+
+  attr_fields = g_strsplit (attribute, " ", -1);
+  n_attr_fields = g_strv_length (attr_fields);
+
+  if (n_attr_fields < 3) {
+    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                 "Need at least three fields in a SDP crypto attribute: %s",
+                 attribute);
+
+    return NULL;
+  }
+
+  tag_str = &attr_fields[0][9]; /* 9 is the length of "a=crypto:" */
+
+  /* leading zeros MUST NOT be used */
+  if (*tag_str == '0') {
+    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                 "Tag must not have a leading zero: %s",
+                 tag_str);
+
+    return NULL;
+  }
+
+  tag = (int) strtol (tag_str, NULL, 10);
+  if (tag == 0) {
+    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                 "Tag set to 0: %s", tag_str);
+
+    return NULL;
+  }
+
+  if (tag < 0) {
+    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                 "Tag must be positive: %s", tag_str);
+
+    return NULL;
+  }
+
+  if (tag >= 1000000000) {
+    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                 "Tag must have a maximum of 9 digits: %s", tag_str);
+
+    return NULL;
+  }
+
+  /* f.e. attr_fields[1] = "AES_CM_128_HMAC_SHA1_32" */
+  if (g_strcmp0 (attr_fields[1], "AES_CM_128_HMAC_SHA1_32") == 0)
+    crypto_suite = CALLS_SRTP_SUITE_AES_128_SHA1_32;
+  else if (g_strcmp0 (attr_fields[1], "AES_CM_128_HMAC_SHA1_80") == 0)
+    crypto_suite = CALLS_SRTP_SUITE_AES_128_SHA1_80;
+  else
+    crypto_suite = CALLS_SRTP_SUITE_UNKNOWN; /* error */
+
+  /* key infos are split by ';' */
+  key_info_strv = g_strsplit (attr_fields[2], ";", -1);
+  n_key_params = g_strv_length (key_info_strv);
+
+  /* libsrtp supports a maximum of 16 master keys */
+  if (n_key_params > 16) {
+    g_set_error (error, G_IO_ERROR, G_IO_ERROR_FAILED,
+                 "More than 16 keys are not supported by libsrtp");
+
+    return NULL;
+  }
+
+  need_mki = n_key_params > 1;
+
+  attr = calls_srtp_crypto_attribute_new (n_key_params);
+
+  attr->tag = tag;
+  attr->crypto_suite = crypto_suite;
+
+  for (guint i = 0; i < n_key_params; i++) {
+    char *key_info; /* srtp-key-info       = key-salt ["|" lifetime] ["|" mki] */
+    g_auto (GStrv) key_info_fields = NULL;
+    guint n_key_infos;
+    guint key_info_lifetime_index;
+    guint key_info_mki_index;
+    calls_srtp_crypto_key_param *key_param = &attr->key_params[i];
+
+    if (!g_str_has_prefix (key_info_strv[i], "inline:")) {
+      attr_invalid = TRUE;
+      err_msg = g_strdup_printf ("Key method not 'inline': %s", key_info_strv[i]);
+      break;
+    }
+
+    key_info = &key_info_strv[i][7]; /* 7 is the length of "inline:" */
+    key_info_fields = g_strsplit (key_info, "|", -1);
+
+    n_key_infos = g_strv_length (key_info_fields);
+
+    key_param->b64_keysalt = g_strdup (key_info_fields[0]);
+
+    if (n_key_infos == 1) {
+      key_info_lifetime_index = 0;
+      key_info_mki_index = 0;
+    } else if (n_key_infos == 2) {
+      /* either MKI or lifetime */
+      if (g_strstr_len (key_info_fields[1], -1, ":")) {
+        key_info_lifetime_index = 0;
+        key_info_mki_index = 1;
+      } else {
+        key_info_lifetime_index = 1;
+        key_info_mki_index = 0;
+      }
+    } else if (n_key_infos == 3) {
+      key_info_lifetime_index = 1;
+      key_info_mki_index = 2;
+    } else {
+      /* invalid */
+      attr_invalid = TRUE;
+      err_msg = g_strdup_printf ("Unexpected number of key-info fields: %s", key_info);
+      break;
+    }
+
+    /* lifetime type */
+    if (key_info_lifetime_index) {
+      char *lifetime_number;
+      char *endptr;
+      if (g_str_has_prefix (key_info_fields[key_info_lifetime_index], "2^")) {
+        key_param->lifetime_type = CALLS_SRTP_LIFETIME_AS_POWER_OF_TWO;
+        lifetime_number = &key_info_fields[key_info_lifetime_index][2]; /* 2 is the length of "2^" */
+      } else {
+        key_param->lifetime_type = CALLS_SRTP_LIFETIME_AS_DECIMAL_NUMBER;
+        lifetime_number = key_info_fields[key_info_lifetime_index];
+      }
+
+      if (*lifetime_number == '0') {
+        attr_invalid = TRUE;
+        err_msg = g_strdup_printf ("Leading zero in lifetime: %s",
+                                   key_info_fields[key_info_lifetime_index]);
+        break;
+      }
+
+      key_param->lifetime = g_ascii_strtoull (lifetime_number, &endptr, 10);
+      if (key_param->lifetime == 0) {
+        attr_invalid = TRUE;
+        err_msg = g_strdup_printf ("Lifetime set to zero: %s",
+                                   key_info_fields[key_info_lifetime_index]);
+        break;
+      }
+
+      if (*endptr != '\0') {
+        attr_invalid = TRUE;
+        err_msg = g_strdup_printf ("Non numeric characters in lifetime: %s",
+                                   key_info_fields[key_info_lifetime_index]);
+        break;
+      }
+
+      /* out of bounds check will be performed during validation of the attribute */
+    }
+
+    if (need_mki && key_info_mki_index == 0) {
+      attr_invalid = TRUE;
+      err_msg = g_strdup_printf ("MKI needed, but not found: %s", key_info);
+      break;
+    }
+
+    if (need_mki) {
+      g_auto (GStrv) mki_split = g_strsplit (key_info_fields[key_info_mki_index], ":", -1);
+      guint n_mki = g_strv_length (mki_split);
+      guint64 mki;
+      guint64 mki_length;
+      char *endptr;
+
+      if (n_mki != 2) {
+        attr_invalid = TRUE;
+        err_msg = g_strdup_printf ("MKI field not separated into two fields by colon: %s",
+                                   key_info_fields[key_info_mki_index]);
+        break;
+      }
+
+      /* no leading zero allowed */
+      if (*mki_split[0] == '0') {
+        attr_invalid = TRUE;
+        err_msg = g_strdup_printf ("Leading zero in MKI: %s", mki_split[0]);
+        break;
+      }
+
+      if (*mki_split[1] == '0') {
+        attr_invalid = TRUE;
+        err_msg = g_strdup_printf ("Leading zero in MKI length: %s", mki_split[1]);
+        break;
+      }
+
+      mki = g_ascii_strtoull (mki_split[0], &endptr, 10);
+      if (mki == 0) {
+        attr_invalid = TRUE;
+        err_msg = g_strdup_printf ("MKI set to 0: %s", mki_split[0]);
+        break;
+      }
+
+      if (*endptr != '\0') {
+        attr_invalid = TRUE;
+        err_msg = g_strdup_printf ("Non numeric characters found in MKI: %s", mki_split[0]);
+        break;
+      }
+
+      mki_length = g_ascii_strtoull (mki_split[1], &endptr, 10);
+      /* number of bytes of the MKI field in the SRTP packet */
+      if (mki_length == 0 || mki_length > 128) {
+        attr_invalid = TRUE;
+        err_msg = g_strdup_printf ("MKI length not between 0 and 128: %s", mki_split[1]);
+        break;
+      }
+
+      if (*endptr != '\0') {
+        attr_invalid = TRUE;
+        err_msg = g_strdup_printf ("Non numeric characters found in MKI length: %s", mki_split[1]);
+        break;
+      }
+
+      key_param->mki = mki;
+      key_param->mki_length = (guint) mki_length;
+    }
+
+  }
+
+  if (attr_invalid) {
+    calls_srtp_crypto_attribute_free (attr);
+    g_set_error_literal (error, G_IO_ERROR, G_IO_ERROR_FAILED, err_msg);
+
+    return NULL;
+  }
+
+  /* TODO session parameters */
+
+  if (!validate_crypto_attribute (attr, error)) {
+    calls_srtp_crypto_attribute_free (attr);
+
+    return NULL;
+
+  }
+
+  return attr;
+}
+
+/**
+ * calls_srtp_print_sdp_crypto_attribute:
+ * @attr: Structured crypto attribute
+ * @error: A #GError
+ *
+ * Returns: (transfer full): Textual representation of crypto attribute
+ * or %NULL if attribute contains invalid data.
+ */
+char *
+calls_srtp_print_sdp_crypto_attribute (calls_srtp_crypto_attribute *attr,
+                                       GError                     **error)
+{
+  const char *crypto_suite;
+  GString *attr_str;
+
+  if (!validate_crypto_attribute (attr, error))
+    return NULL;
+
+  if (attr->crypto_suite == CALLS_SRTP_SUITE_AES_128_SHA1_32)
+    crypto_suite = "AES_CM_128_HMAC_SHA1_32";
+  else if (attr->crypto_suite == CALLS_SRTP_SUITE_AES_128_SHA1_80)
+    crypto_suite = "AES_CM_128_HMAC_SHA1_80";
+  else
+    return NULL;
+
+  attr_str = g_string_sized_new (96); /* minimal string length is 82 */
+
+  g_string_append_printf (attr_str, "a=crypto:%d %s ",
+                          attr->tag, crypto_suite);
+
+  /* key parameters */
+  for (guint i = 0; i < attr->n_key_params; i++) {
+    calls_srtp_crypto_key_param *key_param = &attr->key_params[i];
+
+    g_string_append_printf (attr_str, "inline:%s", key_param->b64_keysalt);
+    if (key_param->lifetime_type == CALLS_SRTP_LIFETIME_AS_DECIMAL_NUMBER)
+      g_string_append_printf (attr_str, "|%" G_GINT64_FORMAT, key_param->lifetime);
+    if (key_param->lifetime_type == CALLS_SRTP_LIFETIME_AS_POWER_OF_TWO)
+      g_string_append_printf (attr_str, "|2^%" G_GINT64_FORMAT, key_param->lifetime);
+
+    if (key_param->mki > 0) {
+      g_string_append_printf(attr_str, "|%" G_GUINT64_FORMAT ":%u",
+                             key_param->mki, key_param->mki_length);
+    }
+
+    if (i + 1 < attr->n_key_params)
+      g_string_append (attr_str, ";");
+  }
+
+  /* TODO session parameters */
+
+  return g_string_free (attr_str, FALSE);
+}
+
+/**
+ * calls_srtp_crypto_attribute_new:
+ * @n_key_params: The number of key parameters
+ *
+ * Returns: (transfer full): A new empty #calls_srtp_crypto_attribute
+ * with @n_key_params key parameters allocated. Key parameters must be set either
+ * manually or by using calls_srtp_crypto_attribute_init_keys().
+ *
+ * Free the attribute with calls_srtp_crypto_attribute_free() after you are done.
+ */
+calls_srtp_crypto_attribute *
+calls_srtp_crypto_attribute_new (guint n_key_params)
+{
+  calls_srtp_crypto_attribute *attr;
+
+  g_return_val_if_fail (n_key_params > 0 || n_key_params < 16, NULL);
+
+  attr = g_new0 (calls_srtp_crypto_attribute, 1);
+  attr->key_params = g_new0 (calls_srtp_crypto_key_param, n_key_params);
+  attr->n_key_params = n_key_params;
+
+  return attr;
+}
+
+/**
+ * calls_srtp_crypto_attribute_init_keys:
+ * @attr: A #calls_srtp_crypto_attribute
+ *
+ * Generate key material and set sane default parameters for each
+ * key parameter.
+ */
+gboolean
+calls_srtp_crypto_attribute_init_keys (calls_srtp_crypto_attribute *attr)
+{
+  gsize key_size;
+  gboolean need_mki;
+
+  g_return_val_if_fail (attr, FALSE);
+
+  key_size = get_key_size_for_suite (attr->crypto_suite);
+  if (key_size == 0)
+    return FALSE;
+
+  need_mki = attr->n_key_params > 1;
+
+  for (uint i = 0; i < attr->n_key_params; i++) {
+    g_autofree guchar *key = calls_srtp_generate_key_salt (key_size);
+    g_free (attr->key_params[i].b64_keysalt);
+    attr->key_params[i].b64_keysalt = g_base64_encode (key, key_size);
+
+    if (need_mki) {
+      attr->key_params[i].mki = i + 1;
+      attr->key_params[i].mki_length = 4;
+    }
+  }
+
+  return TRUE;
+}
+
+/**
+ * calls_srtp_crypto_attribute_free:
+ * @attr: A #calls_srtp_crypto_attribute
+ *
+ * Frees all memory allocated for @attr.
+ */
+void
+calls_srtp_crypto_attribute_free (calls_srtp_crypto_attribute *attr)
+{
+  for (guint i = 0; i < attr->n_key_params; i++) {
+    g_free (attr->key_params[i].b64_keysalt);
+  }
+
+  g_free (attr->key_params);
+  g_free (attr->b64_fec_key);
+  g_free (attr);
+}
diff --git a/plugins/sip/calls-srtp-utils.h b/plugins/sip/calls-srtp-utils.h
new file mode 100644
index 00000000..72d37ca7
--- /dev/null
+++ b/plugins/sip/calls-srtp-utils.h
@@ -0,0 +1,106 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * This file is part of Calls.
+ *
+ * Calls 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * Calls 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 Calls.  If not, see <http://www.gnu.org/licenses/>.
+ *
+ * Author: Evangelos Ribeiro Tzaras <evangelos tzaras puri sm>
+ *
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ *
+ */
+
+#pragma once
+
+#include <glib.h>
+
+G_BEGIN_DECLS
+
+typedef enum {
+  CALLS_SRTP_SUITE_UNKNOWN = 0,
+  CALLS_SRTP_SUITE_AES_128_SHA1_32,
+  CALLS_SRTP_SUITE_AES_128_SHA1_80,
+} calls_srtp_crypto_suite;
+
+
+typedef enum {
+  CALLS_SRTP_FEC_ORDER_UNSET = 0,
+  CALLS_SRTP_FEC_ORDER_FEC_SRTP,
+  CALLS_SRTP_FEC_ORDER_SRTP_FEC
+} calls_srtp_fec_order;
+
+
+typedef enum {
+  CALLS_SRTP_LIFETIME_UNSET = 0,
+  CALLS_SRTP_LIFETIME_AS_DECIMAL_NUMBER,
+  CALLS_SRTP_LIFETIME_AS_POWER_OF_TWO
+} calls_srtp_lifetime_type;
+
+
+typedef struct {
+  char                    *b64_keysalt;
+  calls_srtp_lifetime_type lifetime_type;
+  /* maximum lifetime: SRTP 2^48 packets; SRTCP 2^31 packets; libsrtp uses a hardcoded limit of 2^48 */
+  guint64                  lifetime;
+
+  guint64                  mki;
+  guint                    mki_length;
+} calls_srtp_crypto_key_param;
+
+
+typedef struct {
+  gint                         tag;
+  calls_srtp_crypto_suite      crypto_suite;
+
+  calls_srtp_crypto_key_param *key_params;
+  guint                        n_key_params;
+
+  /** session parameters
+   * For more information see https://datatracker.ietf.org/doc/html/rfc4568#section-6.3
+   * KDR (key derivation rate) defaults to 0; declarative parameter
+   *  decimal integer in {1,2,...,24}, denotes a power of two. 0 means unspecified
+   *  defaulting to a single initial key derivation
+   * UNENCRYPTED_SRTCP, UNENCRYPTED_SRTP; negotiated parameter
+   * UNAUTHENTICATED_SRTP; negotiated parameter
+   * FEC_ORDER; declarative parameter
+   * FEC_KEY separate key-params for forward error correction; declarative parameter
+   * WSH (Window Size Hint); declarative parameter; MAY be ignored
+   */
+  gint                 kdr;
+  gboolean             unencrypted_srtp;
+  gboolean             unencrypted_srtcp;
+  gboolean             unauthenticated_srtp;
+  calls_srtp_fec_order fec_order; /* FEC in RTP: RFC2733 */
+  char                *b64_fec_key; /* TODO this should also be an array of calls_srtp_crypto_key_param */
+  guint                window_size_hint;
+} calls_srtp_crypto_attribute;
+
+
+guchar                      *calls_srtp_generate_key_salt              (gsize length);
+guchar                      *calls_srtp_generate_key_salt_for_suite    (calls_srtp_crypto_suite suite);
+calls_srtp_crypto_attribute *calls_srtp_parse_sdp_crypto_attribute     (const char *attr,
+                                                                        GError    **error);
+char                        *calls_srtp_print_sdp_crypto_attribute     (calls_srtp_crypto_attribute *attr,
+                                                                        GError                     **error);
+calls_srtp_crypto_attribute *calls_srtp_crypto_attribute_new           (guint n_key_params);
+gboolean                     calls_srtp_crypto_attribute_init_keys     (calls_srtp_crypto_attribute *attr);
+void                         calls_srtp_crypto_attribute_free          (calls_srtp_crypto_attribute *attr);
+char                        *calls_srtp_generate_crypto_for_offer      (void);
+
+
+G_DEFINE_AUTOPTR_CLEANUP_FUNC (calls_srtp_crypto_attribute, calls_srtp_crypto_attribute_free)
+
+
+G_END_DECLS
diff --git a/plugins/sip/meson.build b/plugins/sip/meson.build
index 4e97c843..9b3e1dc2 100644
--- a/plugins/sip/meson.build
+++ b/plugins/sip/meson.build
@@ -51,6 +51,7 @@ sip_sources = files(
     'calls-sip-media-manager.c', 'calls-sip-media-manager.h',
     'calls-sip-media-pipeline.c', 'calls-sip-media-pipeline.h',
     'calls-sip-account-widget.c', 'calls-sip-account-widget.h',
+    'calls-srtp-utils.c', 'calls-srtp-utils.h',
     'gst-rfc3551.c', 'gst-rfc3551.h',
   ]
 )
diff --git a/tests/meson.build b/tests/meson.build
index 9171ce82..1f68d2c1 100644
--- a/tests/meson.build
+++ b/tests/meson.build
@@ -123,6 +123,21 @@ t = executable('sip', test_sources,
                )
 test('sip', t, env: test_env)
 
+test_sources = [ 'test-srtp.c' ]
+test_sources += sip_sources
+t = executable('srtp', test_sources,
+               c_args : test_cflags,
+               link_args: test_link_args,
+               pie: true,
+               link_with : [calls_vala, libcalls],
+               dependencies: [calls_deps, sip_deps],
+               include_directories : [
+                   calls_includes,
+                   sip_include,
+                 ]
+               )
+test('srtp', t, env: test_env)
+
 test_sources = [ 'test-util.c' ]
 t = executable('util', test_sources,
                c_args : test_cflags,
diff --git a/tests/test-srtp.c b/tests/test-srtp.c
new file mode 100644
index 00000000..1d352311
--- /dev/null
+++ b/tests/test-srtp.c
@@ -0,0 +1,236 @@
+/*
+ * Copyright (C) 2022 Purism SPC
+ *
+ * SPDX-License-Identifier: GPL-3.0+
+ *
+ * Author: Evangelos Ribeiro Tzaras <evangelos tzaras puri sm>
+ */
+
+#include "calls-srtp-utils.h"
+
+#include <gtk/gtk.h>
+
+
+static void
+assert_attr_eq (calls_srtp_crypto_attribute *a,
+                calls_srtp_crypto_attribute *b)
+{
+  g_assert_cmpint (a->tag, ==, b->tag);
+  g_assert_cmpint (a->crypto_suite, ==, b->crypto_suite);
+  g_assert_cmpuint (a->n_key_params, ==, b->n_key_params);
+
+  for (guint i = 0; i < a->n_key_params; i++) {
+    g_assert_cmpstr (a->key_params[i].b64_keysalt, ==,
+                     b->key_params[i].b64_keysalt);
+    g_assert_cmpuint (a->key_params[i].mki, ==,
+                      b->key_params[i].mki);
+    g_assert_cmpuint (a->key_params[i].mki_length, ==,
+                      b->key_params[i].mki_length);
+  }
+
+  g_assert_cmpint (a->unencrypted_srtp, ==, b->unencrypted_srtp);
+  g_assert_cmpint (a->unauthenticated_srtp, ==, b->unauthenticated_srtp);
+  g_assert_cmpint (a->unencrypted_srtcp, ==, b->unencrypted_srtcp);
+
+  g_assert_cmpint (a->kdr, ==, b->kdr);
+  g_assert_cmpint (a->fec_order, ==, b->fec_order);
+  g_assert_cmpint (a->window_size_hint, ==, b->window_size_hint);
+}
+
+
+static void
+test_crypto_attribute_validity (void)
+{
+  g_autoptr (calls_srtp_crypto_attribute) attr = NULL;
+  char *attr_str;
+  guchar *key_salt;
+  char *tmp_str;
+
+  /* single key parameter */
+
+  attr = calls_srtp_crypto_attribute_new (1);
+
+  attr->tag = 1;
+  attr->crypto_suite = CALLS_SRTP_SUITE_UNKNOWN;
+
+  g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL));
+
+  attr->crypto_suite = CALLS_SRTP_SUITE_AES_128_SHA1_32;
+  key_salt = calls_srtp_generate_key_salt (30);
+  attr->key_params[0].b64_keysalt = g_base64_encode (key_salt, 30);
+  g_free (key_salt);
+
+  attr_str = calls_srtp_print_sdp_crypto_attribute (attr, NULL);
+  g_assert_true (attr_str);
+
+  g_free (attr_str);
+
+  /* tag out of bounds */
+  attr->tag = 0;
+  g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL));
+
+  attr->tag = 1000000000;
+  g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL));
+
+  attr->tag = 1;
+
+  /* lifetime out of bounds */
+  attr->key_params[0].lifetime = 49;
+  attr->key_params[0].lifetime_type = CALLS_SRTP_LIFETIME_AS_POWER_OF_TWO;
+
+  g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL));
+
+  attr->key_params[0].lifetime = 1ULL << 48;
+  attr->key_params[0].lifetime_type = CALLS_SRTP_LIFETIME_AS_DECIMAL_NUMBER;
+
+  g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL));
+
+  attr->key_params[0].lifetime = 0;
+  attr->key_params[0].lifetime_type = CALLS_SRTP_LIFETIME_UNSET;
+
+  /* MKI without length */
+  attr->key_params[0].mki = 1;
+  attr->key_params[0].mki_length = 0;
+
+  g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL));
+
+  attr->key_params[0].mki_length = 129;
+
+  g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL));
+
+  attr->key_params[0].mki = 0;
+  attr->key_params[0].mki_length = 4;
+
+  g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL));
+
+  /* missing key */
+  attr->key_params[0].mki_length = 0;
+  g_clear_pointer (&attr->key_params[0].b64_keysalt, g_free);
+
+  g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL));
+
+  /* wrong key length */
+  key_salt = calls_srtp_generate_key_salt (29);
+  attr->key_params[0].b64_keysalt = g_base64_encode (key_salt, 29);
+  g_free (key_salt);
+
+  g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL));
+
+  calls_srtp_crypto_attribute_free (attr);
+
+  /* multiple key parameters */
+
+  attr = calls_srtp_crypto_attribute_new (4);
+  attr->tag = 12;
+  attr->crypto_suite = CALLS_SRTP_SUITE_AES_128_SHA1_80;
+
+  calls_srtp_crypto_attribute_init_keys (attr);
+  attr->key_params[0].lifetime = 31;
+  attr->key_params[0].lifetime_type = CALLS_SRTP_LIFETIME_AS_POWER_OF_TWO;
+
+  attr_str = calls_srtp_print_sdp_crypto_attribute (attr, NULL);
+  g_assert_true (attr_str);
+
+  g_free (attr_str);
+  calls_srtp_crypto_attribute_free (attr);
+
+
+  /* same key */
+  attr = calls_srtp_crypto_attribute_new (2);
+  attr->tag = 1;
+  tmp_str = attr->key_params[1].b64_keysalt;
+  attr->key_params[1].b64_keysalt = attr->key_params[0].b64_keysalt;
+
+  g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL));
+
+  attr->key_params[1].b64_keysalt = tmp_str;
+
+  /* same MKI */
+  attr->key_params[0].mki = 1;
+  attr->key_params[1].mki = 1;
+
+  g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL));
+
+  /* different MKI lengths */
+  attr->key_params[1].mki = 2;
+  attr->key_params[0].mki_length = 1;
+  attr->key_params[1].mki_length = 3;
+
+  g_assert_null (calls_srtp_print_sdp_crypto_attribute (attr, NULL));
+
+  g_assert_null (calls_srtp_print_sdp_crypto_attribute (NULL, NULL));
+}
+
+
+static void
+test_parse (void)
+{
+  g_autoptr (calls_srtp_crypto_attribute) attr_simple = NULL;
+  g_autoptr (calls_srtp_crypto_attribute) attr_parsed_simple = NULL;
+  g_autofree char *attr_simple_str = NULL;
+  g_autofree char *attr_simple_str_expected = NULL;
+
+  g_autoptr (calls_srtp_crypto_attribute) attr_multi = NULL;
+  g_autoptr (calls_srtp_crypto_attribute) attr_parsed_multi = NULL;
+  g_autofree char *attr_multi_str = NULL;
+  g_autofree char *attr_multi_str_expected = NULL;
+
+  g_autofree guchar *key_salt = NULL;
+
+  /* single key */
+
+  attr_simple = calls_srtp_crypto_attribute_new (1);
+  key_salt = calls_srtp_generate_key_salt (30);
+  attr_simple->tag = 1;
+  attr_simple->crypto_suite = CALLS_SRTP_SUITE_AES_128_SHA1_32;
+  attr_simple->key_params[0].b64_keysalt = g_base64_encode (key_salt, 30);
+
+  attr_simple_str = calls_srtp_print_sdp_crypto_attribute (attr_simple, NULL);
+  attr_simple_str_expected =
+    g_strdup_printf ("a=crypto:%d AES_CM_128_HMAC_SHA1_32 inline:%s",
+                     attr_simple->tag,
+                     attr_simple->key_params[0].b64_keysalt);
+
+  g_assert_cmpstr (attr_simple_str, ==, attr_simple_str_expected);
+
+  attr_parsed_simple = calls_srtp_parse_sdp_crypto_attribute (attr_simple_str, NULL);
+  assert_attr_eq (attr_simple, attr_parsed_simple);
+
+  /* multiple keys */
+
+  attr_multi = calls_srtp_crypto_attribute_new (2);
+  attr_multi->tag = 42;
+  attr_multi->crypto_suite = CALLS_SRTP_SUITE_AES_128_SHA1_80;
+  calls_srtp_crypto_attribute_init_keys (attr_multi);
+
+  attr_multi_str = calls_srtp_print_sdp_crypto_attribute (attr_multi, NULL);
+  attr_multi_str_expected =
+    g_strdup_printf ("a=crypto:%d AES_CM_128_HMAC_SHA1_80 "
+                     "inline:%s|%" G_GUINT64_FORMAT ":%u;"
+                     "inline:%s|%" G_GUINT64_FORMAT ":%u",
+                     attr_multi->tag,
+                     attr_multi->key_params[0].b64_keysalt,
+                     attr_multi->key_params[0].mki,
+                     attr_multi->key_params[0].mki_length,
+                     attr_multi->key_params[1].b64_keysalt,
+                     attr_multi->key_params[1].mki,
+                     attr_multi->key_params[1].mki_length);
+
+  g_assert_cmpstr (attr_multi_str, ==, attr_multi_str_expected);
+
+  attr_parsed_multi = calls_srtp_parse_sdp_crypto_attribute (attr_multi_str, NULL);
+  assert_attr_eq (attr_multi, attr_parsed_multi);
+}
+
+
+int
+main (int   argc,
+      char *argv[])
+{
+  gtk_test_init (&argc, &argv, NULL);
+
+  g_test_add_func ("/Calls/SRTP-SDP/crypto_attribute_validity", test_crypto_attribute_validity);
+  g_test_add_func ("/Calls/SRTP-SDP/parse", test_parse);
+
+  return g_test_run ();
+}


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