1<?php
2/**
3 * Provides an API for encrypting and decrypting small pieces of data with the
4 * use of a shared key stored in a cookie.
5 *
6 * Copyright 1999-2016 Horde LLC (http://www.horde.org/)
7 *
8 * See the enclosed file COPYING for license information (LGPL). If you
9 * did not receive this file, see http://www.horde.org/licenses/lgpl21.
10 *
11 * @author   Chuck Hagenbuch <chuck@horde.org>
12 * @author   Michael Slusarz <slusarz@horde.org>
13 * @category Horde
14 * @license  http://www.horde.org/licenses/lgpl21 LGPL
15 * @package  Secret
16 */
17class Horde_Secret
18{
19    /** Generic, default keyname. */
20    const DEFAULT_KEY = 'generic';
21
22    /**
23     * Configuration parameters.
24     *
25     * @var array
26     */
27    protected $_params = array(
28        'cookie_domain' => '',
29        'cookie_path' => '',
30        'cookie_ssl' => false,
31        'session_name' => 'horde_secret'
32    );
33
34    /**
35     * Cipher cache.
36     *
37     * @var array
38     */
39    protected $_cipherCache = array();
40
41    /**
42     * Key cache.
43     *
44     * @var array
45     */
46    protected $_keyCache = array();
47
48    /**
49     * Constructor.
50     *
51     * @param array $params  Configuration parameters:
52     *   - cookie_domain: (string) The cookie domain.
53     *   - cookie_path: (string) The cookie path.
54     *   - cookie_ssl: (boolean) Only transmit cookie securely?
55     *   - session_name: (string) The cookie session name.
56     */
57    public function __construct($params = array())
58    {
59        $this->_params = array_merge($this->_params, $params);
60    }
61
62    /**
63     * Take a small piece of data and encrypt it with a key.
64     *
65     * @param string $key      The key to use for encryption.
66     * @param string $message  The plaintext message.
67     *
68     * @return string  The ciphertext message.
69     * @throws Horde_Secret_Exception
70     */
71    public function write($key, $message)
72    {
73        $message = strval($message);
74        return (strlen($key) && strlen($message))
75            ? $this->_getCipherOb($key)->encrypt($message)
76            : '';
77    }
78
79    /**
80     * Decrypt a message encrypted with write().
81     *
82     * @param string $key      The key to use for decryption.
83     * @param string $message  The ciphertext message.
84     *
85     * @return string  The plaintext message.
86     * @throws Horde_Secret_Exception
87     */
88    public function read($key, $ciphertext)
89    {
90        $ciphertext = strval($ciphertext);
91        return (strlen($key) && strlen($ciphertext))
92            ? $this->_getCipherOb($key)->decrypt($ciphertext)
93            : '';
94    }
95
96    /**
97     * Returns the cached crypt object.
98     *
99     * @param string $key  The key to use for [de|en]cryption. Only the first
100     *                     56 bytes of this string is used.
101     *
102     * @return Horde_Crypt_Blowfish  The crypt object.
103     * @throws Horde_Secret_Exception
104     */
105    protected function _getCipherOb($key)
106    {
107        if (!is_string($key)) {
108            throw new Horde_Secret_Exception('Key must be a string', Horde_Secret_Exception::KEY_NOT_STRING);
109        }
110
111        if (!strlen($key)) {
112            throw new Horde_Secret_Exception('Key must be non-zero.', Horde_Secret_Exception::KEY_ZERO_LENGTH);
113        }
114
115        $key = substr($key, 0, 56);
116
117        $idx = hash('md5', $key);
118        if (!isset($this->_cipherCache[$idx])) {
119            $this->_cipherCache[$idx] = new Horde_Crypt_Blowfish($key);
120        }
121
122        return $this->_cipherCache[$idx];
123    }
124
125    /**
126     * Generate a secret key (for encryption), either using a random
127     * string and storing it in a cookie if the user has cookies
128     * enabled, or munging some known values if they don't.
129     *
130     * @param string $keyname  The name of the key to set.
131     *
132     * @return string  The secret key that has been generated.
133     */
134    public function setKey($keyname = self::DEFAULT_KEY)
135    {
136        $set = true;
137
138        if (isset($_COOKIE[$this->_params['session_name']])) {
139            if (isset($_COOKIE[$keyname . '_key'])) {
140                $key = $_COOKIE[$keyname . '_key'];
141                $set = false;
142            } else {
143                $key = $_COOKIE[$keyname . '_key'] = strval(new Horde_Support_Randomid());
144            }
145        } else {
146            $key = session_id();
147        }
148
149        if ($set) {
150            $this->_setCookie($keyname, $key);
151        }
152
153        return $key;
154    }
155
156    /**
157     * Return a secret key, either from a cookie, or if the cookie
158     * isn't there, assume we are using a munged version of a known
159     * base value.
160     *
161     * @param string $keyname  The name of the key to get.
162     *
163     * @return string  The secret key.
164     */
165    public function getKey($keyname = self::DEFAULT_KEY)
166    {
167        if (!isset($this->_keyCache[$keyname])) {
168            if (isset($_COOKIE[$keyname . '_key'])) {
169                $key = $_COOKIE[$keyname . '_key'];
170            } else {
171                $key = session_id();
172                $this->_setCookie($keyname, $key);
173            }
174
175            $this->_keyCache[$keyname] = $key;
176        }
177
178        return $this->_keyCache[$keyname];
179    }
180
181    /**
182     * Clears a secret key entry from the current cookie.
183     *
184     * @param string $keyname  The name of the key to clear.
185     *
186     * @return boolean  True if key existed, false if not.
187     */
188    public function clearKey($keyname = self::DEFAULT_KEY)
189    {
190        if (isset($_COOKIE[$this->_params['session_name']]) &&
191            isset($_COOKIE[$keyname . '_key'])) {
192            $this->_setCookie($keyname, false);
193            return true;
194        }
195
196        return false;
197    }
198
199    /**
200     * Sets the cookie with the given keyname/key.
201     *
202     * @param string $keyname  The name of the key to set.
203     * @param string $key      The key to use for encryption.
204     */
205    protected function _setCookie($keyname, $key)
206    {
207        @setcookie(
208            $keyname . '_key',
209            $key,
210            0,
211            $this->_params['cookie_path'],
212            $this->_params['cookie_domain'],
213            $this->_params['cookie_ssl'],
214            true
215        );
216
217        if ($key === false) {
218            unset($_COOKIE[$keyname . '_key'], $this->_keyCache[$keyname]);
219        } else {
220            $_COOKIE[$keyname . '_key'] = $this->_keyCache[$keyname] = $key;
221        }
222    }
223
224}
225