/* Copyright (c) 2003-2018 Dovecot authors, see the included COPYING file */ #include "lib.h" #include "array.h" #include "md5.h" #include "hash.h" #include "hex-binary.h" #include "base64.h" #include "hostpid.h" #include "hmac.h" #include "pkcs5.h" #include "hash-method.h" #include "str.h" #include "strescape.h" #include "var-expand.h" #include "var-expand-private.h" #include #include #define TABLE_LAST(t) \ ((t)->key == '\0' && (t)->long_key == NULL) struct var_expand_modifier { char key; const char *(*func)(const char *, struct var_expand_context *); }; static ARRAY(struct var_expand_extension_func_table) var_expand_extensions; static const char * m_str_lcase(const char *str, struct var_expand_context *ctx ATTR_UNUSED) { return t_str_lcase(str); } static const char * m_str_ucase(const char *str, struct var_expand_context *ctx ATTR_UNUSED) { return t_str_ucase(str); } static const char * m_str_escape(const char *str, struct var_expand_context *ctx ATTR_UNUSED) { return str_escape(str); } static const char * m_str_hex(const char *str, struct var_expand_context *ctx ATTR_UNUSED) { unsigned long long l; if (str_to_ullong(str, &l) < 0) l = 0; return t_strdup_printf("%llx", l); } static const char * m_str_reverse(const char *str, struct var_expand_context *ctx ATTR_UNUSED) { size_t len = strlen(str); char *p, *rev; rev = t_malloc_no0(len + 1); rev[len] = '\0'; for (p = rev + len - 1; *str != '\0'; str++) *p-- = *str; return rev; } static const char *m_str_hash(const char *str, struct var_expand_context *ctx) { unsigned int value = str_hash(str); string_t *hash = t_str_new(20); if (ctx->width != 0) { value %= ctx->width; ctx->width = 0; } str_printfa(hash, "%x", value); while ((int)str_len(hash) < ctx->offset) str_insert(hash, 0, "0"); ctx->offset = 0; return str_c(hash); } static const char * m_str_newhash(const char *str, struct var_expand_context *ctx) { string_t *hash = t_str_new(20); unsigned char result[MD5_RESULTLEN]; unsigned int i; uint64_t value = 0; md5_get_digest(str, strlen(str), result); for (i = 0; i < sizeof(value); i++) { value <<= 8; value |= result[i]; } if (ctx->width != 0) { value %= ctx->width; ctx->width = 0; } str_printfa(hash, "%x", (unsigned int)value); while ((int)str_len(hash) < ctx->offset) str_insert(hash, 0, "0"); ctx->offset = 0; return str_c(hash); } static const char * m_str_md5(const char *str, struct var_expand_context *ctx ATTR_UNUSED) { unsigned char digest[16]; md5_get_digest(str, strlen(str), digest); return binary_to_hex(digest, sizeof(digest)); } static const char * m_str_ldap_dn(const char *str, struct var_expand_context *ctx ATTR_UNUSED) { string_t *ret = t_str_new(256); while (*str != '\0') { if (*str == '.') str_append(ret, ",dc="); else str_append_c(ret, *str); str++; } return str_free_without_data(&ret); } static const char * m_str_trim(const char *str, struct var_expand_context *ctx ATTR_UNUSED) { size_t len; len = strlen(str); while (len > 0 && i_isspace(str[len-1])) len--; return t_strndup(str, len); } #define MAX_MODIFIER_COUNT 10 static const struct var_expand_modifier modifiers[] = { { 'L', m_str_lcase }, { 'U', m_str_ucase }, { 'E', m_str_escape }, { 'X', m_str_hex }, { 'R', m_str_reverse }, { 'H', m_str_hash }, { 'N', m_str_newhash }, { 'M', m_str_md5 }, { 'D', m_str_ldap_dn }, { 'T', m_str_trim }, { '\0', NULL } }; static int var_expand_short(const struct var_expand_table *table, char key, const char **var_r, const char **error_r) { const struct var_expand_table *t; if (table != NULL) { for (t = table; !TABLE_LAST(t); t++) { if (t->key == key) { *var_r = t->value != NULL ? t->value : ""; return 1; } } } /* not found */ if (key == '%') { *var_r = "%"; return 1; } if (*error_r == NULL) *error_r = t_strdup_printf("Unknown variable '%%%c'", key); *var_r = t_strdup_printf("UNSUPPORTED_VARIABLE_%c", key); return 0; } static int var_expand_hash(struct var_expand_context *ctx, const char *key, const char *field, const char **result_r, const char **error_r) { enum { FORMAT_HEX, FORMAT_HEX_UC, FORMAT_BASE64 } format = FORMAT_HEX; const char *p = strchr(key, ';'); const char *const *args = NULL; const char *algo = key; const char *value; int ret; if (p != NULL) { algo = t_strcut(key, ';'); args = t_strsplit(p+1, ","); } const struct hash_method *method; if (strcmp(algo, "pkcs5") == 0) { method = hash_method_lookup("sha256"); } else if ((method = hash_method_lookup(algo)) == NULL) { return 0; } string_t *field_value = t_str_new(64); string_t *salt = t_str_new(64); string_t *tmp = t_str_new(method->digest_size); if ((ret = var_expand_long(ctx, field, strlen(field), &value, error_r)) < 1) { return ret; } str_append(field_value, value); /* default values */ unsigned int rounds = 1; unsigned int truncbits = 0; if (strcmp(algo, "pkcs5") == 0) { rounds = 2048; str_append(salt, field); } while(args != NULL && *args != NULL) { const char *k = t_strcut(*args, '='); const char *value = strchr(*args, '='); if (value == NULL) { args++; continue; } else { value++; } if (strcmp(k, "rounds") == 0) { if (str_to_uint(value, &rounds)<0) { *error_r = t_strdup_printf( "Cannot parse hash arguments:" "'%s' is not number for rounds", value); return -1; } if (rounds < 1) { *error_r = t_strdup_printf( "Cannot parse hash arguments:" "rounds must be at least 1"); return -1; } } else if (strcmp(k, "truncate") == 0) { if (str_to_uint(value, &truncbits)<0) { *error_r = t_strdup_printf( "Cannot parse hash arguments:" "'%s' is not number for truncbits", value); return -1; } truncbits = I_MIN(truncbits, method->digest_size*8); } else if (strcmp(k, "salt") == 0) { str_truncate(salt, 0); if (var_expand_with_funcs(salt, value, ctx->table, ctx->func_table, ctx->context, error_r) < 0) { return -1; } break; } else if (strcmp(k, "format") == 0) { if (strcmp(value, "hex") == 0) { format = FORMAT_HEX; } else if (strcmp(value, "hexuc") == 0){ format = FORMAT_HEX_UC; } else if (strcmp(value, "base64") == 0) { format = FORMAT_BASE64; } else { *error_r = t_strdup_printf( "Cannot parse hash arguments:" "'%s' is not supported format", value); return -1; } } args++; } str_truncate(tmp, 0); if (strcmp(algo, "pkcs5") == 0) { if (pkcs5_pbkdf(PKCS5_PBKDF2, method, field_value->data, field_value->used, salt->data, salt->used, rounds, HMAC_MAX_CONTEXT_SIZE, tmp) != 0) { *error_r = "Cannot hash: PKCS5_PBKDF2 failed"; return -1; } } else { void *context = t_malloc_no0(method->context_size); str_append_str(tmp, field_value); for(;rounds>0;rounds--) { method->init(context); if (salt->used > 0) method->loop(context, salt->data, salt->used); method->loop(context, tmp->data, tmp->used); unsigned char *digest = buffer_get_modifiable_data(tmp, NULL); method->result(context, digest); if (tmp->used != method->digest_size) buffer_set_used_size(tmp, method->digest_size); } } if (truncbits > 0) buffer_truncate_rshift_bits(tmp, truncbits); switch(format) { case FORMAT_HEX: *result_r = binary_to_hex(tmp->data, tmp->used); return 1; case FORMAT_HEX_UC: *result_r = binary_to_hex(tmp->data, tmp->used); return 1; case FORMAT_BASE64: { string_t *dest = t_str_new(64); base64_encode(tmp->data, tmp->used, dest); *result_r = str_c(dest); return 1; } } i_unreached(); } static int var_expand_func(const struct var_expand_func_table *func_table, const char *key, const char *data, void *context, const char **var_r, const char **error_r) { const char *value = NULL; int ret; if (strcmp(key, "env") == 0) { value = getenv(data); *var_r = value != NULL ? value : ""; return 1; } if (func_table != NULL) { for (; func_table->key != NULL; func_table++) { if (strcmp(func_table->key, key) == 0) { ret = func_table->func(data, context, &value, error_r); *var_r = value != NULL ? value : ""; return ret; } } } if (*error_r == NULL) *error_r = t_strdup_printf("Unknown variable '%%%s'", key); *var_r = t_strdup_printf("UNSUPPORTED_VARIABLE_%s", key); return 0; } static int var_expand_try_extension(struct var_expand_context *ctx, const char *key, const char *data, const char **var_r, const char **error_r) { int ret; const char *sep = strchr(key, ';'); if (sep == NULL) sep = key + strlen(key); /* try with extensions */ const struct var_expand_extension_func_table *f; array_foreach(&var_expand_extensions, f) { /* ensure we won't match abbreviations */ size_t len = sep-key; if (strncasecmp(key, f->key, len) == 0 && f->key[len] == '\0') return f->func(ctx, key, data, var_r, error_r); } if ((ret = var_expand_func(ctx->func_table, key, data, ctx->context, var_r, error_r)) == 0) { *error_r = t_strdup_printf("Unknown variable '%%%s'", key); } return ret; } int var_expand_long(struct var_expand_context *ctx, const void *key_start, size_t key_len, const char **var_r, const char **error_r) { const struct var_expand_table *t; const char *key, *value = NULL; int ret = 1; if (ctx->table != NULL) { for (t = ctx->table; !TABLE_LAST(t); t++) { if (t->long_key != NULL && strncmp(t->long_key, key_start, key_len) == 0 && t->long_key[key_len] == '\0') { *var_r = t->value != NULL ? t->value : ""; return 1; } } } key = t_strndup(key_start, key_len); /* built-in variables: */ switch (key_len) { case 3: if (strcmp(key, "pid") == 0) value = my_pid; else if (strcmp(key, "uid") == 0) value = dec2str(geteuid()); else if (strcmp(key, "gid") == 0) value = dec2str(getegid()); break; case 8: if (strcmp(key, "hostname") == 0) value = my_hostname; break; } if (value == NULL) { const char *data = strchr(key, ':'); if (data != NULL) key = t_strdup_until(key, data++); else data = ""; ret = var_expand_try_extension(ctx, key, data, &value, error_r); if (ret <= 0 && value == NULL) { value = ""; } } *var_r = value; return ret; } int var_expand_with_funcs(string_t *dest, const char *str, const struct var_expand_table *table, const struct var_expand_func_table *func_table, void *context, const char **error_r) { const struct var_expand_modifier *m; const char *var; struct var_expand_context ctx; const char *(*modifier[MAX_MODIFIER_COUNT]) (const char *, struct var_expand_context *); const char *end; unsigned int i, modifier_count; size_t len; int ret, final_ret = 1; *error_r = NULL; i_zero(&ctx); ctx.table = table; ctx.func_table = func_table; ctx.context = context; for (; *str != '\0'; str++) { if (*str != '%') str_append_c(dest, *str); else { int sign = 1; str++; /* reset per-field modifiers */ ctx.offset = 0; ctx.width = 0; ctx.zero_padding = FALSE; /* [.][] */ if (*str == '-') { sign = -1; str++; } if (*str == '0') { ctx.zero_padding = TRUE; str++; } while (*str >= '0' && *str <= '9') { ctx.width = ctx.width*10 + (*str - '0'); str++; } if (*str == '.') { ctx.offset = sign * ctx.width; sign = 1; ctx.width = 0; str++; /* if offset was prefixed with zero (or it was plain zero), just ignore that. zero padding is done with the width. */ ctx.zero_padding = FALSE; if (*str == '0') { ctx.zero_padding = TRUE; str++; } if (*str == '-') { sign = -1; str++; } while (*str >= '0' && *str <= '9') { ctx.width = ctx.width*10 + (*str - '0'); str++; } ctx.width = sign * ctx.width; } modifier_count = 0; while (modifier_count < MAX_MODIFIER_COUNT) { modifier[modifier_count] = NULL; for (m = modifiers; m->key != '\0'; m++) { if (m->key == *str) { /* @UNSAFE */ modifier[modifier_count] = m->func; str++; break; } } if (modifier[modifier_count] == NULL) break; modifier_count++; } if (*str == '\0') break; var = NULL; if (*str == '{' && strchr(str, '}') != NULL) { /* %{long_key} */ unsigned int ctr = 1; bool escape = FALSE; end = str; while(*++end != '\0' && ctr > 0) { if (!escape && *end == '\\') { escape = TRUE; continue; } if (escape) { escape = FALSE; continue; } if (*end == '{') ctr++; if (*end == '}') ctr--; } if (ctr == 0) /* it needs to come back a bit */ end--; /* if there is no } it will consume rest of the string */ len = end - (str + 1); ret = var_expand_long(&ctx, str+1, len, &var, error_r); str = end; } else { ret = var_expand_short(ctx.table, *str, &var, error_r); } i_assert(var != NULL); if (final_ret > ret) final_ret = ret; if (ret <= 0) str_append(dest, var); else { for (i = 0; i < modifier_count; i++) var = modifier[i](var, &ctx); if (ctx.offset < 0) { /* if offset is < 0 then we want to start at the end */ size_t len = strlen(var); size_t offset_from_end = -ctx.offset; if (len > offset_from_end) var += len - offset_from_end; } else { while (*var != '\0' && ctx.offset > 0) { ctx.offset--; var++; } } if (ctx.width == 0) str_append(dest, var); else if (!ctx.zero_padding) { if (ctx.width < 0) ctx.width = strlen(var) - (-ctx.width); str_append_max(dest, var, ctx.width); } else { /* %05d -like padding. no truncation. */ ssize_t len = strlen(var); while (len < ctx.width) { str_append_c(dest, '0'); ctx.width--; } str_append(dest, var); } } } } return final_ret; } int var_expand(string_t *dest, const char *str, const struct var_expand_table *table, const char **error_r) { return var_expand_with_funcs(dest, str, table, NULL, NULL, error_r); } static bool var_get_key_range_full(const char *str, unsigned int *idx_r, unsigned int *size_r) { const struct var_expand_modifier *m; unsigned int i = 0; /* [.][] */ while ((str[i] >= '0' && str[i] <= '9') || str[i] == '-') i++; if (str[i] == '.') { i++; while ((str[i] >= '0' && str[i] <= '9') || str[i] == '-') i++; } do { for (m = modifiers; m->key != '\0'; m++) { if (m->key == str[i]) { i++; break; } } } while (m->key != '\0'); if (str[i] != '{') { /* short key */ *idx_r = i; *size_r = str[i] == '\0' ? 0 : 1; return FALSE; } else { unsigned int depth = 1; bool escape = FALSE; /* long key */ *idx_r = ++i; for (; str[i] != '\0'; i++) { if (!escape && str[i] == '\\') { escape = TRUE; continue; } if (escape) { escape = FALSE; continue; } if (str[i] == '{') depth++; if (str[i] == '}') { if (--depth==0) break; } } *size_r = i - *idx_r; return TRUE; } } char var_get_key(const char *str) { unsigned int idx, size; if (var_get_key_range_full(str, &idx, &size)) return '{'; return str[idx]; } void var_get_key_range(const char *str, unsigned int *idx_r, unsigned int *size_r) { (void)var_get_key_range_full(str, idx_r, size_r); } static bool var_has_long_key(const char **str, const char *long_key) { const char *start, *end; start = strchr(*str, '{'); i_assert(start != NULL); end = strchr(++start, '}'); if (end == NULL) return FALSE; if (strncmp(start, long_key, end-start) == 0 && long_key[end-start] == '\0') return TRUE; *str = end; return FALSE; } bool var_has_key(const char *str, char key, const char *long_key) { char c; for (; *str != '\0'; str++) { if (*str == '%' && str[1] != '\0') { str++; c = var_get_key(str); if (c == key && key != '\0') return TRUE; if (c == '{' && long_key != NULL) { if (var_has_long_key(&str, long_key)) return TRUE; } } } return FALSE; } void var_expand_extensions_deinit(void) { array_free(&var_expand_extensions); } void var_expand_extensions_init(void) { i_array_init(&var_expand_extensions, 32); /* put all hash methods there */ for(const struct hash_method **meth = hash_methods; *meth != NULL; meth++) { struct var_expand_extension_func_table *func = array_append_space(&var_expand_extensions); func->key = (*meth)->name; func->func = var_expand_hash; } /* pkcs5 */ struct var_expand_extension_func_table *func = array_append_space(&var_expand_extensions); func->key = "pkcs5"; func->func = var_expand_hash; /* if */ func = array_append_space(&var_expand_extensions); func->key = "if"; func->func = var_expand_if; } void var_expand_register_func_array(const struct var_expand_extension_func_table *funcs) { for(const struct var_expand_extension_func_table *ptr = funcs; ptr->key != NULL; ptr++) { i_assert(*ptr->key != '\0'); array_push_front(&var_expand_extensions, ptr); } } void var_expand_unregister_func_array(const struct var_expand_extension_func_table *funcs) { for(const struct var_expand_extension_func_table *ptr = funcs; ptr->key != NULL; ptr++) { i_assert(ptr->func != NULL); for(unsigned int i = 0; i < array_count(&var_expand_extensions); i++) { const struct var_expand_extension_func_table *func = array_idx(&var_expand_extensions, i); if (strcasecmp(func->key, ptr->key) == 0) { array_delete(&var_expand_extensions, i, 1); } } } } struct var_expand_table * var_expand_merge_tables(pool_t pool, const struct var_expand_table *a, const struct var_expand_table *b) { ARRAY(struct var_expand_table) table; size_t a_size = var_expand_table_size(a); size_t b_size = var_expand_table_size(b); p_array_init(&table, pool, a_size + b_size + 1); for(size_t i=0; ikey = a[i].key; entry->value = p_strdup(pool, a[i].value); entry->long_key = p_strdup(pool, a[i].long_key); } for(size_t i=0; ikey = b[i].key; entry->value = p_strdup(pool, b[i].value); entry->long_key = p_strdup(pool, b[i].long_key); } array_append_zero(&table); return array_front_modifiable(&table); }