1: <?php
2: /*
3: * Copyright (C) 2013 Bryan Nielsen - All Rights Reserved
4: *
5: * Author: Bryan Nielsen (bnielsen1965@gmail.com)
6: *
7: *
8: * This file is part of cryptUser.
9: * cryptUser is free software: you can redistribute it and/or modify
10: * it under the terms of the GNU General Public License as published by
11: * the Free Software Foundation, either version 3 of the License, or
12: * (at your option) any later version.
13: *
14: * cryptUser is distributed in the hope that it will be useful,
15: * but WITHOUT ANY WARRANTY; without even the implied warranty of
16: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17: * GNU General Public License for more details.
18: *
19: * You should have received a copy of the GNU General Public License
20: * along with cryptUser. If not, see <http://www.gnu.org/licenses/>.
21: */
22:
23: namespace BSN\CryptUser;
24: use Exception;
25:
26: require_once 'SSLKey.php';
27:
28: /**
29: * CryptUser object.
30: *
31: * @author Bryan Nielsen, bnielsen1965@gmail.com
32: * @copyright Copyright 2013, Bryan Nielsen
33: */
34: class CryptUser {
35: /**
36: * Class constants
37: */
38: const ACL_ADMIN_FLAG = 1;
39: const ACL_ACTIVE_FLAG = 2;
40: const ACL_ALL_FLAGS = 3; // OR all flags together
41:
42:
43: /**
44: * Class properties
45: */
46: private $username;
47: private $password;
48: private $passwordHash; // hashed password
49: private $authenticated; // user authenticated flag
50: private $primaryKey; // SSLKey instance
51: private $sslKey; // PEM formatted private key and certificate
52: private $flags; // user flag settings
53: private $dataSource;
54:
55:
56: /**
57: * Object constructor
58: * @param string username The username for this user.
59: * @param string $password The password for this user.
60: * @param string $dataSource An object to provide the user data.
61: */
62: function __construct($username, $password, $dataSource = NULL) {
63: $this->username = $username;
64: $this->password = $password;
65: $this->dataSource = $dataSource;
66: $this->clearAllACLFlags();
67: $this->sslKey = '';
68:
69: if (!empty($this->dataSource)) $this->loadUser();
70: }
71:
72:
73: /**
74: * Load this user defined by this object from the data source
75: * @return boolean TRUE on success, FALSE on failure
76: */
77: private function loadUser() {
78: // retrieve user's data from the data source
79: $userData = $this->dataSource->getUserByName($this->username);
80:
81: // if user data returned then process
82: if ($userData) {
83: // use data values for this user
84: $this->username = $userData['username'];
85: $this->passwordHash = $userData['passwordHash'];
86: $this->flags = $userData['flags'];
87: $this->sslKey = $userData['sslKey'];
88:
89: // authenticate user by checking data source password hash with a hash of the provided password
90: if ($this->passwordHash == $this->hashPassword($this->password, $this->passwordHash)) {
91: // authentication passed
92: $this->authenticated = TRUE;
93:
94: // set the user's primary SSL key
95: $this->setPrimaryKey(SSLKey::parsePrivateKey($this->sslKey), SSLKey::parseCertificate($this->sslKey));
96: }
97: else {
98: // authentication failed
99: $this->authenticated = FALSE;
100: }
101:
102: // load successful
103: return TRUE;
104: }
105:
106: // failed to load the user
107: return FALSE;
108: }
109:
110:
111: /**
112: * Save this user to the data source
113: * @return boolean TRUE on success, FALSE on failure.
114: */
115: public function saveUser() {
116: return $this->dataSource->saveUser(array(
117: 'username' => $this->username,
118: 'passwordHash' => $this->passwordHash,
119: 'sslKey' => $this->sslKey,
120: 'flags' => $this->flags
121: ));
122: }
123:
124:
125: /**
126: * Complete the steps to establish this as a new user in the data source.
127: * @return boolean TRUE on success, FALSE on failure
128: */
129: public function newUser() {
130: // if user does not exist then create
131: if ($this->dataSource->getUserByName($this->username) === FALSE) {
132: // use the change password function to set the password hash and create a primary key
133: $this->changePassword($this->password);
134:
135: // save the new user
136: $this->saveUser();
137:
138: return TRUE;
139: }
140: else {
141: // user already exists
142: return FALSE;
143: }
144: }
145:
146:
147: /**
148: * Get the username string for this user.
149: * @return string The username value.
150: */
151: public function getUsername() {
152: return $this->username;
153: }
154:
155:
156: /**
157: * Set the primary SSLKey to use with this user.
158: * @param string $key Optional PEM encoded private key to use in generating the SSLKey.
159: * @param string $certificate Optional PEM encoded certificate to use in generating the SSLKey.
160: * @return boolean TRUE on success, FALSE on failure.
161: */
162: public function setPrimaryKey($key = NULL, $certificate = NULL) {
163: // create primary key
164: $this->primaryKey = new SSLKey($this->password, $key, $certificate);
165:
166: // if the provided key or certificate were empty then use the new values from the new primary key
167: if (empty($key) || empty($certificate)) $this->sslKey = $this->primaryKey->getPrivateKey() . $this->primaryKey->getCertificate();
168:
169: return TRUE;
170: }
171:
172:
173: /**
174: * Sets the Access Control Level flags specified in the flagMask variable.
175: * @param integer $flagMask Integer containing the binary flags to turn on.
176: * @return boolean TRUE on success, FALSE on failure.
177: */
178: public function setACLFlags($flagMask) {
179: $this->flags = $this->flags | $flagMask;
180: return TRUE;
181: }
182:
183:
184: /**
185: * Clears the Access Control Level flags specified in flagMask.
186: * @param integer $flagMask Integer containing the binary flags to turn off.
187: * @return boolean TRUE on success, FALSE on failure.
188: */
189: public function clearACLFlags($flagMask) {
190: $this->flags = $this->flags & ~$flagMask;
191:
192: return TRUE;
193: }
194:
195:
196: /**
197: * Set all possible ACL flags on this user.
198: */
199: public function setAllACLFlags() {
200: $this->flags = CryptUser::ACL_ALL_FLAGS;
201: }
202:
203:
204: /**
205: * Clear all possible ACL flags on this user.
206: */
207: public function clearAllACLFlags() {
208: $this->flags = 0;
209: }
210:
211:
212: /**
213: * Test if the specified flags are set as specified in the flagMask argument.
214: * @param integer $flagmask Integer containing the binary flags to test.
215: * @return boolean TRUE if the specified flags are set, FALSE if any of the flags are not set.
216: */
217: public function isACLFlagSet($flagMask) {
218: return ($flagMask & $this->flags ? TRUE : FALSE);
219: }
220:
221:
222: /**
223: * Determine if this user is an admin
224: * @return boolean TRUE if admin, FALSE if not.
225: */
226: public function isAdmin() {
227: // check flags against admin mask
228: return $this->isACLFlagSet(CryptUser::ACL_ADMIN_FLAG);
229: }
230:
231:
232: /**
233: * Determine if this user account is active.
234: * @return boolean TRUE if active, FALSE if not.
235: */
236: public function isActive() {
237: // check flags against active mask
238: return $this->isACLFlagSet(CryptUser::ACL_ACTIVE_FLAG);
239: }
240:
241:
242: /**
243: * Check if this user is authenticated.
244: * @return boolean TRUE if authenticated, FALSE if not.
245: */
246: public function isAuthenticated() {
247: return ($this->authenticated ? TRUE : FALSE);
248: }
249:
250:
251: /**
252: * Change the user's password and the SSL Key elements that rely on the password.
253: * @param string $password The new password to use.
254: * @param string $callback Optional callback function used by the application for
255: * post password change maintenance like re-encryption of data. The callback must
256: * accept two CryptUser objects, the first object is the CryptUser with the new
257: * password and encryption key, the second object is the Cryptuser with the old
258: * password and encryption key.
259: * @return boolean TRUE on success, FALSE on failure.
260: */
261: public function changePassword($password, $callback = NULL) {
262: // if a callback is provided then prepare to use callback
263: if (!empty($callback)) {
264: // if this user is not authenticated then decryption will not be possible so we fail
265: if (!$this->isAuthenticated()) return FALSE;
266:
267: // create a clone of this user to be sent to callback
268: $oldCryptUser = clone $this;
269: }
270:
271: $this->password = $password;
272: $this->passwordHash = $this->hashPassword($password);
273:
274: // set a new primary key
275: $this->setPrimaryKey();
276:
277: // set the sslKey value using the new primary key
278: $this->sslKey = $this->primaryKey->getPrivateKey() . $this->primaryKey->getCertificate();
279:
280: // if a callback is provided then call now with the old and this new user
281: if (!empty($callback)) call_user_func($callback, $oldCryptUser, $this);
282:
283: // password change complete
284: return TRUE;
285: }
286:
287:
288:
289:
290:
291: /**
292: * Encrypt a package using the primary SSL key pair.
293: * @param mixed $package The package to encrypt.
294: * @return array An array containing the envelope and encrypted package or NULL if an error occurs.
295: */
296: public function encryptPackage($package) {
297: if ($this->primaryKey) {
298: // encrypt the package
299: $encryptedPackage = $this->primaryKey->encryptPackage($package);
300:
301: if ($encryptedPackage) {
302: // use base 64 encoding to make strings safe for various storage mechanisms
303: $encryptedPackage['envelope'] = base64_encode($encryptedPackage['envelope']);
304: $encryptedPackage['package'] = base64_encode($encryptedPackage['package']);
305:
306: return $encryptedPackage;
307: }
308: }
309:
310: return FALSE;
311: }
312:
313:
314: /**
315: * Decrypt the sealed package using the primary SSL key pair.
316: * @param mixed $package An associative array containing 'package' and 'envelope'.
317: * @return mixed The decrypted package or FALSE if there was an error.
318: */
319: public function decryptPackage($package) {
320: if ($this->primaryKey) {
321: return $this->primaryKey->decryptPackage(base64_decode($package['package']), base64_decode($package['envelope']));
322: }
323:
324: return FALSE;
325: }
326:
327:
328:
329: /**
330: * Get the data source used for this user.
331: * @return object The data source object for this user.
332: */
333: public function getDatasource() {
334: return $this->dataSource;
335: }
336:
337:
338: /**
339: * Hash the provided password string.
340: * @param string $password The plain text user password.
341: * @param string $salt Optional salt to use with crypt. The salt must be provided
342: * when performing a password hash check to make sure the hash values come out the
343: * same. If no salt is provided then the best salt for this server will be generated.
344: * @return string The hashed password.
345: */
346: public static function hashPassword($password, $salt = NULL) {
347: // if password empty then return empty
348: if( empty($password) ) return '';
349:
350: if( is_null($salt) ) $salt = cryptUser::bestSalt();
351:
352: return crypt($password, $salt);
353: }
354:
355:
356: /**
357: * Determine the best salt to use for the crypt function on this server.
358: * @return string The salt to be used with crypt.
359: */
360: public static function bestSalt() {
361: if( defined('CRYPT_SHA512') && CRYPT_SHA512 == 1 ) return '$6$' . SSLKey::makePhrase(16) . '$';
362: if( defined('CRYPT_SHA256') && CRYPT_SHA256 == 1 ) return '$5$' . SSLKey::makePhrase(16) . '$';
363: if( defined('CRYPT_BLOWFISH') && CRYPT_BLOWFISH == 1 ) return '$2a$07$' . base64_encode(SSLKey::makePhrase(22)) . '$';
364: if( defined('CRYPT_MD5') && CRYPT_MD5 == 1 ) return '$1$' . SSLKey::makePhrase(12) . '$';
365: if( defined('CRYPT_EXT_DES') && CRYPT_EXT_DES == 1 ) return '_' . SSLKey::makePhrase(8);
366: if( defined('CRYPT_STD_DES') && CRYPT_STD_DES == 1 ) return SSLKey::makePhrase(2);
367: return '';
368: }
369: }
370:
371: