<?php

/**
 * @package JCE
 * @copyright Copyright (c)2009-2013 Nicholas K. Dionysopoulos
 * @copyright Copyright (c)2014 Ryan Demmer
 * @license GNU General Public License version 3, or later
 *
 * @since 2.4
 * 
 * Modified for JCE by Ryan Demmer
 */
// Protection against direct access
defined('_JEXEC') or die();

require_once(dirname(dirname(__FILE__)) . '/classes/encrypt.php');

/**
 * Implements encrypted settings handling features
 */
class WFEncryptHelper {

    /**
     * PBKDF2 Implementation for deriving keys.
     *
     * @param   string   $p   Password
     * @param   string   $s   Salt
     * @param   integer  $kl  Key length
     * @param   integer  $c   Iteration count
     * @param   string   $a   Hash algorithm
     *
     * @return  string  The derived key.
     *
     * @see     http://en.wikipedia.org/wiki/PBKDF2
     * @see     http://www.ietf.org/rfc/rfc2898.txt
     * 
     * @copyright   Copyright (C) 2005 - 2011 Open Source Matters, Inc. All rights reserved.
     */
    protected static function pbkdf2($p, $s, $kl, $c = 10000, $a = 'sha256') {
        // simple md5 version
        if (!function_exists('hash')) {
            $seed = $p . $s;

            $md5 = md5($seed);

            for ($i = 0; $i < $c; $i++) {
                $md5 = md5($md5 . md5(rand(0, 2147483647)));
            }

            return substr($md5, 0, $kl);
        }

        // Hash length.
        $hl = strlen(hash($a, null, true));

        // Key blocks to compute.
        $kb = ceil($kl / $hl);

        // Derived key.
        $dk = '';

        // Create the key.
        for ($block = 1; $block <= $kb; $block++) {
            // Initial hash for this block.
            $ib = $b = hash_hmac($a, $s . pack('N', $block), $p, true);

            // Perform block iterations.
            for ($i = 1; $i < $c; $i++) {
                $ib ^= ($b = hash_hmac($a, $b, $p, true));
            }

            // Append the iterated block.
            $dk .= $ib;
        }

        // Return derived key of correct length.
        return substr($dk, 0, $kl);
    }

    protected static function generateKey() {
        jimport('joomla.crypt.crypt');

        $key = JCrypt::genRandomBytes(32);
        $salt = md5_file(JPATH_SITE . '/configuration.php');

        $key = base64_encode(self::pbkdf2($key, $salt, 32));

        $filecontents = "<?php defined('WF_EDITOR') or die(); define('WF_SERVERKEY', '$key'); ?>";
        $filename = JPATH_COMPONENT_ADMINISTRATOR . '/serverkey.php';

        $result = JFile::write($filename, $filecontents);

        if (!$result) {
            return '';
        } else {
            return base64_decode($key);
        }
    }

    /**
     * Gets the configured server key, automatically loading the server key storage file
     * if required.
     * @return string
     */
    public static function getKey() {
        if (defined('WF_SERVERKEY')) {
            return base64_decode(WF_SERVERKEY);
        }

        $filename = dirname(dirname(__FILE__)) . '/serverkey.php';

        if (file_exists($filename)) {
            include_once $filename;

            if (defined('WF_SERVERKEY')) {
                return base64_decode(WF_SERVERKEY);
            }
        } else {
            return self::generateKey();
        }
    }

    /**
     * Do the server options allow us to use settings encryption?
     * @return bool
     */
    public static function supportsEncryption() {
        // Do we have base64_encode/_decode required for encryption?
        if (!function_exists('base64_encode') || !function_exists('base64_decode')) {
            return false;
        }

        // Pre-requisites met. We can encrypt and decrypt!
        return true;
    }

    /**
     * Gets the preferred encryption mode. Currently, if mcrypt is installed and activated we will
     * use AES128.
     * @return string
     */
    public static function preferredEncryption() {
        if (function_exists('mcrypt_module_open')) {
            return 'AES128';
        } else {
            return 'CTR128';
        }
    }

    /**
     * Encrypts the settings using the automatically detected preferred algorithm
     * @param $settingsINI string The raw settings INI string
     * @return string The encrypted data to store in the database
     */
    public static function encrypt($data, $key = null) {
        // Do we really support encryption?
        if (!self::supportsEncryption())
            return $data;
        // Does any of the preferred encryption engines exist?
        $encryption = self::preferredEncryption();
        if (empty($encryption))
            return $data;
        // Do we have a non-empty key to begin with?
        if (empty($key))
            $key = self::getKey();
        if (empty($key))
            return $data;

        if ($encryption == 'AES128') {
            $encrypted = WFUtilEncrypt::AESEncryptCBC($data, $key, 128);
            if (empty($encrypted)) {
                $encryption = 'CTR128';
            } else {
                // Note: CBC returns the encrypted data as a binary string and requires Base 64 encoding
                $data = '###AES128###' . base64_encode($encrypted);
            }
        }

        if ($encryption == 'CTR128') {
            $encrypted = WFUtilEncrypt::AESEncryptCtr($settingsINI, $key, 128);
            if (empty($encrypted)) {
                $encryption = '';
            } else {
                // Note: CTR returns the encrypted data readily encoded in Base 64 
                $data = '###CTR128###' . $encrypted;
            }
        }

        return $data;
    }

    /**
     * Decrypts the encrypted settings and returns the plaintext INI string
     * @param $encrypted string The encrypted data
     * @return string The decrypted data
     */
    public static function decrypt($encrypted, $key = null) {
        if (substr($encrypted, 0, 12) == '###AES128###') {
            $mode = 'AES128';
        } elseif (substr($encrypted, 0, 12) == '###CTR128###') {
            $mode = 'CTR128';
        } else {
            return $encrypted;
        }

        if (empty($key)) {
            $key = self::getKey();
        }

        $encrypted = substr($encrypted, 12);
        
        switch ($mode) {
            case 'AES128':
                $encrypted = base64_decode($encrypted);
                $decrypted = WFUtilEncrypt::AESDecryptCBC($encrypted, $key, 128);
                break;

            case 'CTR128':
                $decrypted = WFUtilEncrypt::AESDecryptCtr($encrypted, $key, 128);
                break;
        }
        return rtrim($decrypted, "\0");
    }

}
