/** BEGIN COPYRIGHT BLOCK * 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 3 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, see . * * Additional permission under GPLv3 section 7: * * In the following paragraph, "GPL" means the GNU General Public * License, version 3 or any later version, and "Non-GPL Code" means * code that is governed neither by the GPL nor a license * compatible with the GPL. * * You may link the code of this Program with Non-GPL Code and convey * linked combinations including the two, provided that such Non-GPL * Code only links to the code of this Program through those well * defined interfaces identified in the file named EXCEPTION found in * the source code files (the "Approved Interfaces"). The files of * Non-GPL Code may instantiate templates or use macros or inline * functions from the Approved Interfaces without causing the resulting * work to be covered by the GPL. Only the copyright holders of this * Program may make changes or additions to the list of Approved * Interfaces. * * Authors: * Nathaniel McCallum * * Copyright (C) 2013 Red Hat, Inc. * All rights reserved. * END COPYRIGHT BLOCK **/ #include "libotp.h" #include "librfc.h" #include #include #define TOKEN(s) "ipaToken" s #define O(s) TOKEN("OTP" s) #define T(s) TOKEN("TOTP" s) #define IPA_OTP_DEFAULT_TOKEN_STEP 30 #define IPA_OTP_OBJCLS_FILTER "(objectClass=ipaTokenTOTP)" enum otptoken_type { OTPTOKEN_NONE = 0, OTPTOKEN_TOTP, }; struct otptoken { Slapi_ComponentId *plugin_id; Slapi_DN *sdn; struct hotp_token token; enum otptoken_type type; struct { unsigned int step; int offset; } totp; }; static const char *get_basedn(Slapi_DN *dn) { Slapi_DN *suffix = NULL; void *node = NULL; for (suffix = slapi_get_first_suffix(&node, 0); suffix != NULL; suffix = slapi_get_next_suffix(&node, 0)) { if (slapi_sdn_issuffix(dn, suffix)) return (char *) slapi_sdn_get_dn(suffix); } return NULL; } static inline bool is_algo_valid(const char *algo) { static const char *valid_algos[] = { "sha1", "sha256", "sha384", "sha512", NULL }; int i, ret; for (i = 0; valid_algos[i]; i++) { ret = strcasecmp(algo, valid_algos[i]); if (ret == 0) return true; } return false; } static const struct berval *entry_attr_get_berval(const Slapi_Entry* e, const char *type) { Slapi_Attr* attr = NULL; Slapi_Value *v; int ret; ret = slapi_entry_attr_find(e, type, &attr); if (ret != 0 || attr == NULL) return NULL; ret = slapi_attr_first_value(attr, &v); if (ret < 0) return NULL; return slapi_value_get_berval(v); } static bool validate(struct otptoken *token, time_t now, ssize_t step, uint32_t first, const uint32_t *second) { uint32_t tmp; switch (token->type) { case OTPTOKEN_TOTP: step = (now + token->totp.offset) / token->totp.step + step; break; default: return false; } if (!hotp(&token->token, step, &tmp)) return false; if (first != tmp) return false; if (second == NULL) return true; if (!hotp(&token->token, step + 1, &tmp)) return false; return *second == tmp; } static bool writeback(struct otptoken *token, ssize_t step, bool sync) { Slapi_Value *svals[] = { NULL, NULL }; Slapi_PBlock *pb = NULL; Slapi_Mods *mods = NULL; bool success = false; const char *attr; int value; int ret; switch (token->type) { case OTPTOKEN_TOTP: if (!sync) return true; attr = T("clockOffset"); value = token->totp.offset + step * token->totp.step; break; default: return false; } /* Create the value. */ svals[0] = slapi_value_new(); if (slapi_value_set_int(svals[0], value) != 0) goto error; /* Create the mods. */ mods = slapi_mods_new(); slapi_mods_add_mod_values(mods, LDAP_MOD_REPLACE, attr, svals); /* Perform the modification. */ pb = slapi_pblock_new(); slapi_modify_internal_set_pb(pb, slapi_sdn_get_dn(token->sdn), slapi_mods_get_ldapmods_byref(mods), NULL, NULL, token->plugin_id, 0); if (slapi_modify_internal_pb(pb) != 0) goto error; if (slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &ret) != 0) goto error; if (ret != LDAP_SUCCESS) goto error; /* Save our modifications to the object. */ switch (token->type) { case OTPTOKEN_TOTP: token->totp.offset = value; break; default: break; } success = true; error: slapi_pblock_destroy(pb); slapi_mods_free(&mods); return success; } static void otptoken_free(struct otptoken *token) { if (token == NULL) return; slapi_sdn_free(&token->sdn); free(token->token.key.bytes); slapi_ch_free_string(&token->token.algo); free(token); } void otptoken_free_array(struct otptoken **tokens) { if (tokens == NULL) return; for (size_t i = 0; tokens[i] != NULL; i++) otptoken_free(tokens[i]); free(tokens); } static struct otptoken *otptoken_new(Slapi_ComponentId *id, Slapi_Entry *entry) { const struct berval *tmp; struct otptoken *token; char **vals; token = calloc(1, sizeof(struct otptoken)); if (token == NULL) return NULL; token->plugin_id = id; /* Get the token type. */ vals = slapi_entry_attr_get_charray(entry, "objectClass"); if (vals == NULL) goto error; token->type = OTPTOKEN_NONE; for (int i = 0; vals[i] != NULL; i++) { if (strcasecmp(vals[i], "ipaTokenTOTP") == 0) token->type = OTPTOKEN_TOTP; } slapi_ch_array_free(vals); if (token->type == OTPTOKEN_NONE) goto error; /* Get SDN. */ token->sdn = slapi_sdn_dup(slapi_entry_get_sdn(entry)); if (token->sdn == NULL) goto error; /* Get key. */ tmp = entry_attr_get_berval(entry, O("key")); if (tmp == NULL) goto error; token->token.key.len = tmp->bv_len; token->token.key.bytes = malloc(token->token.key.len); if (token->token.key.bytes == NULL) goto error; memcpy(token->token.key.bytes, tmp->bv_val, token->token.key.len); /* Get length. */ token->token.digits = slapi_entry_attr_get_int(entry, O("digits")); if (token->token.digits != 6 && token->token.digits != 8) goto error; /* Get algorithm. */ token->token.algo = slapi_entry_attr_get_charptr(entry, O("algorithm")); if (token->token.algo == NULL) token->token.algo = slapi_ch_strdup("sha1"); if (!is_algo_valid(token->token.algo)) goto error; switch (token->type) { case OTPTOKEN_TOTP: /* Get offset. */ token->totp.offset = slapi_entry_attr_get_int(entry, T("clockOffset")); /* Get step. */ token->totp.step = slapi_entry_attr_get_uint(entry, T("timeStep")); if (token->totp.step == 0) token->totp.step = IPA_OTP_DEFAULT_TOKEN_STEP; break; default: break; } return token; error: otptoken_free(token); return NULL; } static struct otptoken **find(Slapi_ComponentId *id, const char *user_dn, const char *token_dn, const char *intfilter, const char *extfilter) { struct otptoken **tokens = NULL; Slapi_Entry **entries = NULL; Slapi_PBlock *pb = NULL; Slapi_DN *sdn = NULL; char *filter = NULL; size_t count = 0; int result = -1; if (intfilter == NULL) intfilter = ""; if (extfilter == NULL) extfilter = ""; /* Create the filter. */ if (user_dn == NULL) { filter = "(&" IPA_OTP_OBJCLS_FILTER "%s%s)"; filter = slapi_filter_sprintf(filter, intfilter, extfilter); } else { filter = "(&" IPA_OTP_OBJCLS_FILTER "(ipatokenOwner=%s%s)%s%s)"; filter = slapi_filter_sprintf(filter, ESC_AND_NORM_NEXT_VAL, user_dn, intfilter, extfilter); } /* Create the search. */ pb = slapi_pblock_new(); if (token_dn != NULL) { /* Find only the token specified. */ slapi_search_internal_set_pb(pb, token_dn, LDAP_SCOPE_BASE, filter, NULL, 0, NULL, NULL, id, 0); } else { sdn = slapi_sdn_new_dn_byval(user_dn); if (sdn == NULL) goto error; /* Find all user tokens. */ slapi_search_internal_set_pb(pb, get_basedn(sdn), LDAP_SCOPE_SUBTREE, filter, NULL, 0, NULL, NULL, id, 0); } slapi_search_internal_pb(pb); slapi_ch_free_string(&filter); /* Get the results. */ slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_RESULT, &result); if (result != LDAP_SUCCESS) goto error; slapi_pblock_get(pb, SLAPI_PLUGIN_INTOP_SEARCH_ENTRIES, &entries); if (entries == NULL) goto error; /* TODO: Can I get the count another way? */ for (count = 0; entries[count] != NULL; count++) continue; /* Create the array. */ tokens = calloc(count + 1, sizeof(*tokens)); if (tokens == NULL) goto error; for (count = 0; entries[count] != NULL; count++) { tokens[count] = otptoken_new(id, entries[count]); if (tokens[count] == NULL) { otptoken_free_array(tokens); tokens = NULL; goto error; } } error: if (sdn != NULL) slapi_sdn_free(&sdn); slapi_pblock_destroy(pb); return tokens; } struct otptoken **otptoken_find(Slapi_ComponentId *id, const char *user_dn, const char *token_dn, bool active, const char *filter) { static const char template[] = "(|(ipatokenNotBefore<=%04d%02d%02d%02d%02d%02dZ)(!(ipatokenNotBefore=*)))" "(|(ipatokenNotAfter>=%04d%02d%02d%02d%02d%02dZ)(!(ipatokenNotAfter=*)))" "(|(ipatokenDisabled=FALSE)(!(ipatokenDisabled=*)))"; char actfilt[sizeof(template)]; struct tm tm; time_t now; if (!active) return find(id, user_dn, token_dn, NULL, filter); /* Get the current time. */ if (time(&now) == (time_t) -1) return NULL; if (gmtime_r(&now, &tm) == NULL) return NULL; /* Get the current time string. */ if (snprintf(actfilt, sizeof(actfilt), template, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec, tm.tm_year + 1900, tm.tm_mon + 1, tm.tm_mday, tm.tm_hour, tm.tm_min, tm.tm_sec) < 0) return NULL; return find(id, user_dn, token_dn, actfilt, filter); } int otptoken_get_digits(struct otptoken *token) { return token == NULL ? 0 : token->token.digits; } const Slapi_DN *otptoken_get_sdn(struct otptoken *token) { return token->sdn; } bool otptoken_validate(struct otptoken *token, size_t steps, uint32_t code) { time_t now = 0; if (token == NULL) return false; /* We only need the local time for time-based tokens. */ if (token->type == OTPTOKEN_TOTP && time(&now) == (time_t) -1) return false; for (int i = 0; i <= steps; i++) { /* Validate the positive step. */ if (validate(token, now, i, code, NULL)) return writeback(token, i + 1, false); if (i == 0) continue; /* Validate the negative step. */ if (validate(token, now, 0 - i, code, NULL)) return writeback(token, 0 - i + 1, false); } return false; } bool otptoken_validate_string(struct otptoken *token, size_t steps, const char *code, ssize_t len, bool tail) { uint32_t otp; if (token == NULL || code == NULL) return false; if (len < 0) len = strlen(code); if (len < token->token.digits) return false; if (tail) code = &code[len - token->token.digits]; len = token->token.digits; /* * Convert code string to decimal. * * NOTE: We can't use atol() or strtoul() because: * 1. We may have leading zeros (atol() fails here). * 2. Neither support limiting conversion by length. */ otp = 0; for (ssize_t i = 0; i < len; i++) { if (code[i] < '0' || code[i] > '9') return false; otp *= 10; otp += code[i] - '0'; } return otptoken_validate(token, steps, otp); } bool otptoken_sync(struct otptoken * const *tokens, size_t steps, uint32_t first_code, uint32_t second_code) { time_t now = 0; if (tokens == NULL) return false; if (time(&now) == (time_t) -1) return false; for (int i = 0; i <= steps; i++) { for (int j = 0; tokens[j] != NULL; j++) { /* Validate the positive step. */ if (validate(tokens[j], now, i, first_code, &second_code)) return writeback(tokens[j], i + 2, true); if (i == 0) continue; /* Validate the negative step. */ if (validate(tokens[j], now, 0 - i, first_code, &second_code)) return writeback(tokens[j], 0 - i + 2, true); } } return false; }