storing passwords ... help!

Associate
Joined
24 Jul 2004
Posts
1,580
Location
Preston, Lancs
got a tricky one here, so here goes.

I have a hosted php app that posts from my script to peoples blogs.

To do so, I need a username / password for their blog.

Some people have 200+ blogs. This script is designed for ppl with vast numbers of blogs and no desire to retype their password (or indeed even remember it) every time they want to post to one of their blogs.

The problem is that normally I would salt / hash the password, but in this case, I can't think of a way of doing anything one way - and therefore safe.

Encryption appears to be the only solution, but that is very insecure by todays standards. I don't need a Sony PSN issue on my hands!

I would advise anyone using this script to create a dedicated user / pass combo, but ppl are lazy and would use their admin credentials. And then promptly blame me for any problems.

For the record this is not a spam script. Its for affiliate marketers.

Any help gratefully appreciated, I cannot fathom this one. And it's kinda show stopper.
:confused:
 
Soldato
Joined
6 Aug 2007
Posts
2,516
You've not got much choice in the matter. Passwords will have to be stored and you have no choice but to encrypt.

Maybe salt + sha1 / 256 / 512 or whatever floats your boat.

And in the right circumstances, Encryption can be practically uncrackable.
 
Soldato
Joined
7 Apr 2004
Posts
4,212
You've not got much choice in the matter. Passwords will have to be stored and you have no choice but to encrypt.

Maybe salt + sha1 / 256 / 512 or whatever floats your boat.

And in the right circumstances, Encryption can be practically uncrackable.

Hashing != encryption.

Unless you can patch the remote blogs with a proper remote authentication plugin your options are pretty much limited I think. I assume your logging in with standard HTTP POST auth? If so then yer you might as well just store as plain text and secure the box as best you can, there's not a nice solution. Couple that with you not authenticating over SSL and it's a very bad system...
 
Last edited:
Soldato
Joined
7 Apr 2004
Posts
4,212
you should never, ever, ever store as plain text. :rolleyes:

Why the roll eyes? Obviously your statement is correct, but in this case without major changes it isn't an option.

Under the system architecture the OP outlined, show me something better than plaintext?

If you encrypt them, it's no better than plaintext due to requirements of having they key in the same place. Without a better authentication system in place you can only obfuscate, which IMO is a waste of time and equivalent security to plaintext.
 
Associate
Joined
23 Nov 2009
Posts
146
There's a chance an intruder could get the database but not the source code containing the key - it is better than plain text. Marginally so, but that's still a huge step from being 'no better'.
 
Associate
Joined
9 May 2011
Posts
40
Location
Doncaster
If you encrypt them, it's no better than plaintext due to requirements of having they key in the same place.

You could create an API on a different server, which only answers to requests from set IP addresses & possibly with some passphrase and use that to retrieve/store keys?
 
Soldato
Joined
7 Apr 2004
Posts
4,212
There's a chance an intruder could get the database but not the source code containing the key - it is better than plain text. Marginally so, but that's still a huge step from being 'no better'.

True, this would make it slightly harder but it's still very dangerous because if you got write access to the database you could use the script as a decryption oracle and crack everything in seconds. Or if you only had read access you could dump everything and if that script or key is every compromised it's also game over.

The problem is defending a database is only a very small angle on a web server, if you get in any other way it all comes down to that one script. Then there is still the major issue of transmitting in plaintext/http to the remote blog, so if you control any point on the network route you could do something like a DNS spoof to direct the auth to yourself or just sniff the passwords off the wire.

You could create an API on a different server, which only answers to requests from set IP addresses & possibly with some passphrase and use that to retrieve/store keys?

That would still have the same problem though, if you gain control of the server that speaks to the remote one with the API, it has to have the passwords in plaintext before it sends them over the wire. So you hack the box and modify the client API script or monitor the network interfaces/memory/disk etc.

Basically it's all made of obfuscated fail without a proper auth system to the remote blog.
 
Last edited:
Associate
Joined
23 Nov 2009
Posts
146
Let's be honest, if anyone gets a copy of your average hashed database they can brute force it with no hassle (and you gotta love the support cloud computing gives you for tasks like this). Trust nobody, everyone's out to get you etc
 
Soldato
Joined
7 Apr 2004
Posts
4,212
Let's be honest, if anyone gets a copy of your average hashed database they can brute force it with no hassle (and you gotta love the support cloud computing gives you for tasks like this). Trust nobody, everyone's out to get you etc

Depends on what you mean by average I guess, MD5 will fall yes :p but a suitably sized salt e.g 64bits + a good hash other than MD5 and > 160bits is not bruteforceable.
 
Soldato
Joined
3 Jun 2005
Posts
3,066
Location
The South
True, this would make it slightly harder but it's still very dangerous because if you got write access to the database you could use the script as a decryption oracle and crack everything in seconds. Or if you only had read access you could dump everything and if that script or key is every compromised it's also game over.

The problem is defending a database is only a very small angle on a web server, if you get in any other way it all comes down to that one script. Then there is still the major issue of transmitting in plaintext/http to the remote blog, so if you control any point on the network route you could do something like a DNS spoof to direct the auth to yourself or just sniff the passwords off the wire.

Basically it's all made of obfuscated fail without a proper auth system to the remote blog.


You're assuming that the intruder has gained access to the box, in which case you're pretty much screwed in any situation. It's common sense and 101 of development to secure critical data (and the box) as much as possible where ever possible. Everyone knows that nothing is 100% secure and you can only minimise, but if you don't add security you're leaving the door wide open to getting butt ******.

Granted in this situation there is a huge security hole with the data leaving the box and unless you have access to the blog/box with the blog on the only option you have is to minimise this hole as much as possible ie: use SSL/IP restrictions where possible etc.

And if you're worried about the box/scripts getting compromised then personally I’d deploy Zend Guard/IonCube for script security (certainly this is the case of any data critical project I work on) and if you need an extra layer of security on the DB use something like ezNcrypt/Vormetric.
 
Soldato
Joined
27 Sep 2005
Posts
4,624
Location
London innit
1) Store users local site password cryptographically hashed in database to verify logins.

2) When user logs in, store a separate cryptographic hash with different seed in a persistent object store being careful to expire object and end of session. Temporary table would work also.

3) Use the hash from 2) to symmetrically encrypt/decrypt credentials for 3rd party sites

4) If user changes password from 1) then decrypt all 3rd party site passwords using hash from 2) then reencrypt using new hash.

If someone gets access to the object store when a user is logged in then all of their credentials are exposed, so implement all of the above as a web service on a completely separate server with no direct internet access and nothing else running on it.
 
Last edited:
Soldato
Joined
27 Sep 2005
Posts
4,624
Location
London innit
And if you're worried about the box/scripts getting compromised then personally I’d deploy Zend Guard/IonCube for script security (certainly this is the case of any data critical project I work on) and if you need an extra layer of security on the DB use something like ezNcrypt/Vormetric.

And selinux to make sure the webserver can't access any rogue scripts someone managed to upload/create.
 
Soldato
Joined
27 Sep 2005
Posts
4,624
Location
London innit
It's an interesting problem, and one we had a badly broken implementation of at my old workplace.

Pseudo code, fill in the blanks. I'll update it when it's finished. Almost done now. Code will almost certainly need updating for your own libraries, better error handling and cleaned up into a sensible object(s) as it's in R&D mode.

A users password is hashed using the Whirlpool algorithm and a salt that you *must* redefine. Given current computer power this should only be susceptible to a brute force dictionary attack. Passwords are stored using AES512 encryption with a random Initialisation Vector per password. The key for encryption and decryption is a hash of the password + the salt reversed so to recover this key, a user MUST know the password. If you lose your initial password, you lose all your stored passwords, they are not recoverable.

When a user logins in they are given a unique session key, which they need for all subsequent requests. By hashing the user id and the the unique id together you can then retrieve a en/decryption key stored in a separate table. There is no way to correlate the users to sessions without having both the user id and the secret session (the relationship is not stored anywhere other than the client/end user, it is only a way to determine validity).

By using the Init Vector associated with a user and the Key associated with a session you can unencode the stored password.

The primary weakness in the system is you can brute force sessions against users if you have access to the tables, but adding users & credentials adds exponentially to the complexity (and sessions are transient objects so will not be permanently stored).

Hope that helps :)

PHP:
	require_once("Patterns.php");
	require_once("Config.php");
	require_once("Database.php");

	class PasswordStore {
		private $db;
		const cipher = MCRYPT_RIJNDAEL_256;
		const hash = MHASH_WHIRLPOOL;
		const salt = "Well done. Here come the test results: \"You are a horrible person.\" That's what it says: a horrible person. We weren't even testing for that.";
		public function __construct() {
			$this->db = new Database();
			$this->houseKeeping();
		}
		private function hash($string = null, $salt = null) {
			if (!$salt) {
				$salt = $this::salt;
			}
			return bin2hex(mhash($this::hash, $salt . $string));
		}
		private function unique() {
			/* Originally found on php.net - http://php.net/manual/en/function.uniqid.php - credited to Andrew Moore 04-Dec-2009 04:45 
			   Should be replaced with PECL module UUID */ 
			return sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
				// 32 bits for "time_low"
				mt_rand(0, 0xffff), mt_rand(0, 0xffff),
				// 16 bits for "time_mid"
				mt_rand(0, 0xffff),
				// 16 bits for "time_hi_and_version",
				// four most significant bits holds version number 4
				mt_rand(0, 0x0fff) | 0x4000,
				// 16 bits, 8 bits for "clk_seq_hi_res",
				// 8 bits for "clk_seq_low",
				// two most significant bits holds zero and one for variant DCE1.1
				mt_rand(0, 0x3fff) | 0x8000,
				// 48 bits for "node"
				mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
			);
		}
		private function getInitVector($userId = null, $credentialId = null) {
			if ($userId && $credentialId) {
				/* sql fetch init vector from credential table */
			} else {
				$size = mcrypt_get_iv_size($this::cipher, MCRYPT_MODE_CFB);
				$iv = mcrypt_create_iv($size, MCRYPT_DEV_RANDOM);
			}
		}
		private function validSession($userId, $session) {
			$sessionHash = $this->hash($userId . $session);
			$sqlQuery = "SELECT * FROM session WHERE sessionHash = '%sessionHash'";
			$params = array("%sessionHash" => $sessionHash);
			$this->db->prepare($sqlQuery, $params);
			$resultId = $this->db->execute();
			if ($this->db->numRows($resultId) == 1) {
				return true;
			} else {
				return false;
			}
		}
		private function houseKeeping() {
			$sqlQuery = "DELETE FROM session WHERE sessionExpires < %now";
			$params = array("%now", time());
			$this->db->prepare($sqlQuery, $params);
			$this->db->execute();
		}
		public function createSession($userId, $pw = null, $sessionExpires = null) {
			$sqlQuery = "SELECT * FROM userauth WHERE uniqueId = '%userId'";
			$params = array("%userId" => $userId);
			$this->db->prepare($sqlQuery, $params);
			$resultId = $this->db->execute();
			if ($this->db->numRows($resultId) == 1) {
				$row = $this->db->fetchAssoc($resultId);
				if ($row["hashedPassword"] == $this->hash($pw)) {
					$session = $this->unique();
					if (!isset($sessionExpires) || !is_numeric($sessionExpires)) {
						$sessionExpires = 3600; //hardcoding is bad!
					}
					$sessionExpires = time() + $sessionExpires;
					$encryptionKey = $this->hash($pw, strrev($this::salt));
					$sessionHash = $this->hash($userId . $session);
					$sqlQuery = "INSERT INTO session 
									(sessionHash, encryptionKey, sessionExpires) VALUES
									('%sessionHash', '%encryptionKey', %sessionExpires)";
					$params = array("%sessionHash" => $sessionHash, "%encryptionKey" => $encryptionKey, "%sessionExpires" => $sessionExpires);
					$this->db->prepare($sqlQuery, $params);
					$this->db->execute();
					return $session;
				} else {
					// password bad;
				}
			} else {
				// no or duplicate users
			}
		}
		public function expireSession($userId, $session) {
			$sessionHash = $this->hash($userId . $session);
			$sqlQuery = "DELETE FROM session WHERE sessionHash = '%sessionHash'";
			$params = array("%sessionHash" => $sessionHash);
			$this->db->prepare($sqlQuery, $params);
			$this->db->execute();
		}
		public function registerUser($pw = null) {
			if ($pw) {
				$unique = $this->unique();
				$pw = $this->hash($pw);
				echo strlen($pw);
				$sqlQuery = "INSERT INTO userauth
					(uniqueId, hashedPassword, createTime) VALUES
					('%uniqueId', '%hashedPassword', %createTime)";
				$params = array("%uniqueId" => $unique, "%hashedPassword" => $pw, "%createTime" => time());
				$this->db->prepare($sqlQuery, $params);
				$this->db->execute();
				return $unique;
			}
		}
		public function updatePassword($userId, $session, $pw = null) {
			/* foreach credential stored against userId, decrypt with existing key stored in session, and reencrypt using new key 
				$key = $this->hash($pw, strrev($this::salt))
			*/
		}
		public function enumerateCredentials($userId, $session) {
			if ($this->validSession($userId, $session) {
				/* select * from credentials where userId = userId */
			}
		}
		public function createCredential($userId, $session, $cred = null, $pw = null) {
			if ($this->validSession($userId, $session) {
			}
		}
		public function returnCredential($userId, $session, $cred = null, $pw = null) {
			if ($this->validSession($userId, $session) {
			}
			/* get credential $cred for $userId
			   get decryption key for $session
			   decrypt $cred using decryption key */
			//$cleartext = mcrypt_decrypt(MCRYPT_RIJNDAEL_256, 
		}
		public function deleteCredential($userId, $session, $cred = null) {
		}
		public function updateCredential($userId, $session, $cred = null, $pw = null) {
		}
	}

PHP:
CREATE DATABASE /*!32312 IF NOT EXISTS*/`credentials` /*!40100 DEFAULT CHARACTER SET utf8 */;

USE `credentials`;

/*Table structure for table `session` */

DROP TABLE IF EXISTS `session`;

CREATE TABLE `session` (
  `sessionId` bigint(20) NOT NULL AUTO_INCREMENT,
  `sessionHash` varchar(128) DEFAULT NULL,
  `encryptionKey` varchar(128) DEFAULT NULL,
  `sessionExpires` bigint(20) DEFAULT NULL,
  PRIMARY KEY (`sessionId`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

/*Table structure for table `userauth` */

DROP TABLE IF EXISTS `userauth`;

CREATE TABLE `userauth` (
  `userId` bigint(20) NOT NULL AUTO_INCREMENT,
  `uniqueId` varchar(36) NOT NULL,
  `hashedPassword` varchar(128) NOT NULL,
  `createTime` bigint(20) NOT NULL,
  PRIMARY KEY (`userId`)
) ENGINE=InnoDB AUTO_INCREMENT=60 DEFAULT CHARSET=utf8;

/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
 
Last edited:
Back
Top Bottom