<commit_before># -*- coding: utf-8 -*-
#
#  2020-02-27 <NAME> <<EMAIL>>
#             Inial write. Allow tokens to be initialized with an otpkey
#
#  License:  AGPLv3
#  contact:  http://www.privacyidea.org
#
# This code is free software; you can redistribute it and/or
# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
# License as published by the Free Software Foundation; either
# version 3 of the License, or any later version.
#
# This code 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 AFFERO GENERAL PUBLIC LICENSE for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
__doc__ = """This is the implementation of an Indexed Secret-Token.
It is a challenge response token, that asks the user for certain positions
of the secret string.
The user must know the secret and return the characters from the requested positions.

The secret is stored in the encrypted OTP KEY.

The code is tested in tests/test_lib_tokens_indexedsecret

Note that this token has no "check_otp" function. So it is not even
possible to do a "single shot" authentication, since the base class check_otp always
returns -1.
"""

import logging
import datetime
from privacyidea.lib.tokenclass import TokenClass
from privacyidea.lib.policy import SCOPE, ACTION, GROUP, get_action_values_from_options
from privacyidea.lib.log import log_with
from privacyidea.lib import _
from privacyidea.lib.utils import to_unicode
from privacyidea.lib.challenge import get_challenges
from privacyidea.models import Challenge
from privacyidea.lib.decorators import check_token_locked
import random

log = logging.getLogger(__name__)

DEFAULT_CHALLENGE_TEXT = _("Please enter the positions {0!s} from your secret.")
DEFAULT_POSITION_COUNT = 2


class PIIXACTION(object):
    COUNT = "count"
    PRESET_ATTRIBUTE = "preset_attribute"
    FORCE_ATTRIBUTE = "force_attribute"


class IndexedSecretTokenClass(TokenClass):
    """
    Implementation of the Indexed Secret Token Class, that asks the user for certain
    positions in a shared secret.
    """

    def __init__(self, aToken):
        TokenClass.__init__(self, aToken)
        self.set_type(u"indexedsecret")
        self.mode = ['challenge']

    @staticmethod
    def get_class_type():
        """
        return the generic token class identifier
        """
        return "indexedsecret"

    @staticmethod
    def get_class_prefix():
        return "PIIX"

    @staticmethod
    def get_class_info(key=None, ret='all'):
        """
        returns all or a subtree of the token definition

        :param key: subsection identifier
        :type key: string
        :param ret: default return value, if nothing is found
        :type ret: user defined

        :return: subsection if key exists or user defined
        :rtype : s.o.
        """
        res = {'type': 'indexedsecret',
               'title': _('Indexed Secret Token'),
               'description':
                   _('IndexedSecret: Request certain positions of a shared secret from the user.'),
               'user': ['enroll'],
               # This tokentype is enrollable in the UI for...
               'ui_enroll': ["admin", "user"],
               'policy': {SCOPE.AUTH: {
                   ACTION.CHALLENGETEXT: {
                       'type': 'str',
                       'desc': _('Use an alternate challenge text for telling the '
                                 'user which positions of the secret he should enter.'),
                       'group': "Indexed Secret Token"
                   },
                   PIIXACTION.COUNT: {
                       'type': 'int',
                       'desc': _('Number of necessary positions to be answered by the user.'),
                       'group': "Indexed Secret Token"
                   }
               },
                   SCOPE.WEBUI: {
                       PIIXACTION.PRESET_ATTRIBUTE: {
                           'type': 'str',
                           'desc': _("Preset the enrollment with the value of the given attribute."),
                           'group': "token"
                       }
                   },
                   SCOPE.USER: {
                       PIIXACTION.FORCE_ATTRIBUTE: {
                           'type': 'str',
                           'desc': _("The attribute whose value should be force set during enrollment."),
                           'group': "enrollment"
                       }
                   },
                   SCOPE.ADMIN: {
                       PIIXACTION.FORCE_ATTRIBUTE: {
                           'type': 'str',
                           'desc': _("The attribute whose value should be force set during enrollment."),
                           'group': "enrollment"
                       }
                   },
                   SCOPE.ENROLL: {
                       ACTION.MAXTOKENUSER: {
                           'type': 'int',
                           'desc': _("The user may only have this maximum number of indexed secret tokens assigned."),
                           'group': GROUP.TOKEN
                       },
                       ACTION.MAXACTIVETOKENUSER: {
                           'type': 'int',
                           'desc': _("The user may only have this maximum number of active indexed secret"
                                     " tokens assigned."),
                           'group': GROUP.TOKEN
                   }
               }
           }
        }

        if key:
            ret = res.get(key, {})
        else:
            if ret == 'all':
                ret = res

        return ret

    @log_with(log)
    def update(self, param, reset_failcount=True):
        """
        update - process initialization parameters

        :param param: dict of initialization parameters
        :type param: dict

        :return: nothing

        """
        if 'genkey' not in param and 'otpkey' not in param:
            param['genkey'] = 1

        TokenClass.update(self, param, reset_failcount)
        return

    @log_with(log)
    def create_challenge(self, transactionid=None, options=None):
        """
        create a challenge, which is submitted to the user

        :param transactionid: the id of this challenge
        :param options: the request context parameters / data
        :return: tuple of (bool, message and data)
                 bool, if the challenge was created successfully
                 message that is submitted to the user
                 data is preserved in the challenge
                 attributes - additional attributes, which are displayed in the
                    output
        """
        options = options or {}
        return_message = get_action_values_from_options(SCOPE.AUTH,
                                                        "{0!s}_{1!s}".format(self.get_class_type(),
                                                                             ACTION.CHALLENGETEXT),
                                                        options) or DEFAULT_CHALLENGE_TEXT
        position_count = int(get_action_values_from_options(SCOPE.AUTH,
                                                            "{0!s}_{1!s}".format(self.get_class_type(),
                                                                                 PIIXACTION.COUNT),
                                                            options) or DEFAULT_POSITION_COUNT)

        attributes = {'state': transactionid}
        validity = 120

        if self.is_active() is True:
            # We need to get a number of random positions from the secret string
            secret_length = len(self.token.get_otpkey().getKey())<commit_msg>Use the docstring from the parent class.<commit_after># -*- coding: utf-8 -*-
#
#  2020-02-27 <NAME> <<EMAIL>>
#             Inial write. Allow tokens to be initialized with an otpkey
#
#  License:  AGPLv3
#  contact:  http://www.privacyidea.org
#
# This code is free software; you can redistribute it and/or
# modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
# License as published by the Free Software Foundation; either
# version 3 of the License, or any later version.
#
# This code 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 AFFERO GENERAL PUBLIC LICENSE for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with this program.  If not, see <http://www.gnu.org/licenses/>.
#
__doc__ = """This is the implementation of an Indexed Secret-Token.
It is a challenge response token, that asks the user for certain positions
of the secret string.
The user must know the secret and return the characters from the requested positions.

The secret is stored in the encrypted OTP KEY.

The code is tested in tests/test_lib_tokens_indexedsecret

Note that this token has no "check_otp" function. So it is not even
possible to do a "single shot" authentication, since the base class check_otp always
returns -1.
"""

import logging
import datetime
from privacyidea.lib.tokenclass import TokenClass
from privacyidea.lib.policy import SCOPE, ACTION, GROUP, get_action_values_from_options
from privacyidea.lib.log import log_with
from privacyidea.lib import _
from privacyidea.lib.utils import to_unicode
from privacyidea.lib.challenge import get_challenges
from privacyidea.models import Challenge
from privacyidea.lib.decorators import check_token_locked
import random

log = logging.getLogger(__name__)

DEFAULT_CHALLENGE_TEXT = _("Please enter the positions {0!s} from your secret.")
DEFAULT_POSITION_COUNT = 2


class PIIXACTION(object):
    COUNT = "count"
    PRESET_ATTRIBUTE = "preset_attribute"
    FORCE_ATTRIBUTE = "force_attribute"


class IndexedSecretTokenClass(TokenClass):
    """
    Implementation of the Indexed Secret Token Class, that asks the user for certain
    positions in a shared secret.
    """

    def __init__(self, aToken):
        TokenClass.__init__(self, aToken)
        self.set_type(u"indexedsecret")
        self.mode = ['challenge']

    @staticmethod
    def get_class_type():
        """
        return the generic token class identifier
        """
        return "indexedsecret"

    @staticmethod
    def get_class_prefix():
        return "PIIX"

    @staticmethod
    def get_class_info(key=None, ret='all'):
        """
        returns all or a subtree of the token definition

        :param key: subsection identifier
        :type key: string
        :param ret: default return value, if nothing is found
        :type ret: user defined

        :return: subsection if key exists or user defined
        :rtype : s.o.
        """
        res = {'type': 'indexedsecret',
               'title': _('Indexed Secret Token'),
               'description':
                   _('IndexedSecret: Request certain positions of a shared secret from the user.'),
               'user': ['enroll'],
               # This tokentype is enrollable in the UI for...
               'ui_enroll': ["admin", "user"],
               'policy': {SCOPE.AUTH: {
                   ACTION.CHALLENGETEXT: {
                       'type': 'str',
                       'desc': _('Use an alternate challenge text for telling the '
                                 'user which positions of the secret he should enter.'),
                       'group': "Indexed Secret Token"
                   },
                   PIIXACTION.COUNT: {
                       'type': 'int',
                       'desc': _('Number of necessary positions to be answered by the user.'),
                       'group': "Indexed Secret Token"
                   }
               },
                   SCOPE.WEBUI: {
                       PIIXACTION.PRESET_ATTRIBUTE: {
                           'type': 'str',
                           'desc': _("Preset the enrollment with the value of the given attribute."),
                           'group': "token"
                       }
                   },
                   SCOPE.USER: {
                       PIIXACTION.FORCE_ATTRIBUTE: {
                           'type': 'str',
                           'desc': _("The attribute whose value should be force set during enrollment."),
                           'group': "enrollment"
                       }
                   },
                   SCOPE.ADMIN: {
                       PIIXACTION.FORCE_ATTRIBUTE: {
                           'type': 'str',
                           'desc': _("The attribute whose value should be force set during enrollment."),
                           'group': "enrollment"
                       }
                   },
                   SCOPE.ENROLL: {
                       ACTION.MAXTOKENUSER: {
                           'type': 'int',
                           'desc': _("The user may only have this maximum number of indexed secret tokens assigned."),
                           'group': GROUP.TOKEN
                       },
                       ACTION.MAXACTIVETOKENUSER: {
                           'type': 'int',
                           'desc': _("The user may only have this maximum number of active indexed secret"
                                     " tokens assigned."),
                           'group': GROUP.TOKEN
                   }
               }
           }
        }

        if key:
            ret = res.get(key, {})
        else:
            if ret == 'all':
                ret = res

        return ret

    @log_with(log)
    def update(self, param, reset_failcount=True):
        """
        update - process initialization parameters

        :param param: dict of initialization parameters
        :type param: dict

        :return: nothing

        """
        if 'genkey' not in param and 'otpkey' not in param:
            param['genkey'] = 1

        TokenClass.update(self, param, reset_failcount)
        return

    @log_with(log)
    def create_challenge(self, transactionid=None, options=None):
        """
        create a challenge, which is submitted to the user

        :param transactionid: the id of this challenge
        :param options: the request context parameters / data
        :return: tuple of (bool, message, transactionid, attributes)
        :rtype: tuple

        The return tuple builds up like this:
        ``bool`` if submit was successful;
        ``message`` which is displayed in the JSON response;
        additional ``attributes``, which are displayed in the JSON response.
        """
        options = options or {}
        return_message = get_action_values_from_options(SCOPE.AUTH,
                                                        "{0!s}_{1!s}".format(self.get_class_type(),
                                                                             ACTION.CHALLENGETEXT),
                                                        options) or DEFAULT_CHALLENGE_TEXT
        position_count = int(get_action_values_from_options(SCOPE.AUTH,
                                                            "{0!s}_{1!s}".format(self.get_class_type(),
                                                                                 PIIXACTION.COUNT),
                                                            options) or DEFAULT_POSITION_COUNT)

        attributes = {'state': transactionid}
        validity = 120

        if self.is_active() is True:
            # We need to get a number of random positions from the secret string
            secret_length = len(self.token.get_otpkey().getKey())
{'fraction_non_alphanumeric': 0.07888762769580022, 'fraction_numerical': 0.002774624795056123, 'mean_word_length': 1.598132372214941, 'pattern_counts': {'":': 0, '<': 11, '<?xml version=': 0, '>': 11, 'https://': 0, 'lorem ipsum': 0, 'www.': 4, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 32, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5821395', 'n_tokens_mistral': 3918, 'n_tokens_neox': 3741, 'n_words': 1351}