<?php

/**
 * Lifestyle Webconsulting GmbH
 *
 * LICENSE: This Software is the property of Lifestyle Webconsulting GmbH (Aschaffenburg, Germany)
 * and is private by copyright law - it is NOT Freeware.
 *
 * Any unauthorized use of this software without a valid license
 * is a violation of the license agreement and will be prosecuted by
 * civil and criminal law.
 *
 * @copyright  2019 Lifestyle Webconsulting GmbH
 * @link       https://www.life-style.de
 */

namespace SimpleSAML\Module\lifestyle\Webservice\Security;

use InvalidArgumentException;
use LogicException;
use SimpleSAML\Module\lifestyle\Webservice\Api\Configuration\Security;

/**
 * Class MessageDigestPasswordEncoder
 *
 * @copyright  2019 Lifestyle Webconsulting GmbH
 * @link       https://www.life-style.de
 * @package SimpleSAML\Module\lifestyle\Webservice\Security
 */
class MessageDigestPasswordEncoder
{
    const MAX_PASSWORD_LENGTH = 4096;

    /**
     * @var string
     */
    private $algorithm;

    /**
     * @var string
     */
    private $encodeHashAsBase64;

    /**
     * @var string
     */
    private $iterations;

    /**
     * \SimpleSAML\Module\lifestyle\Webservice\Security\MessageDigestPasswordEncoder constructor.
     * @param Security $configuration
     */
    public function __construct(Security $configuration)
    {
        $this->algorithm = $configuration->getAlgorithm();
        $this->encodeHashAsBase64 = $configuration->isEncodeHashAsBase64();
        $this->iterations = $configuration->getIterations();
    }

    /**
     * Encode password
     *
     * @param string $raw Password
     * @param string $salt Password-salt
     * @return string
     * @throws BadCredentialsException
     */
    public function encodePassword($raw, $salt)
    {
        if ($this->isPasswordTooLong($raw)) {
            throw new BadCredentialsException('Invalid password.');
        }

        if (!in_array($this->algorithm, hash_algos(), true)) {
            throw new LogicException(sprintf('The algorithm "%s" is not supported.', $this->algorithm));
        }

        $salted = $this->mergePasswordAndSalt($raw, $salt);
        $digest = hash($this->algorithm, $salted, true);

        // "stretch" hash
        for ($i = 1; $i < $this->iterations; ++$i) {
            $digest = hash($this->algorithm, $digest . $salted, true);
        }

        return $this->encodeHashAsBase64 ? base64_encode($digest) : bin2hex($digest);
    }

    /**
     * Merges a password and a salt.
     *
     * @param string $password the password to be used
     * @param string $salt the salt to be used
     *
     * @return string a merged password and salt
     *
     * @throws InvalidArgumentException
     */
    protected function mergePasswordAndSalt($password, $salt)
    {
        if (empty($salt)) {
            return $password;
        }

        if (false !== strrpos($salt, '{') || false !== strrpos($salt, '}')) {
            throw new InvalidArgumentException('Cannot use { or } in salt.');
        }

        return $password . '{' . $salt . '}';
    }

    /**
     * @param string $password
     * @return bool
     */
    protected function isPasswordTooLong($password)
    {
        return strlen($password) > static::MAX_PASSWORD_LENGTH;
    }
}
