CakePHP Rainbow Table Protection Behaviour

So after looking over some security techniques and discovering the quite interestingly named Rainbow table password cracking mechanism I decided to look into a way around this table password cracker. The default way that CakePHP hashes its passwords into the database is via hashing from a single salt string in core.php. This is a lot better than no salt string, however if someone was keen/bored/lame enough to get to work a rainbow table based on your single salt string then all the passwords/hashed-data stored in your tables would become readable.
A way around this is to add a unique salt string per record, along with the CakePHP’s salt string from core.php. This was a rainbow table would have to be constructed based on a per record basis, and not just on a table. This would mean that the lamer would have to wait a few lifetimes to crack all the passwords in your database table, or wait a long time to crack a single user’s. So my initial behaviour is as follows, and it does require some work arounds:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
<?php
 
class GreyTablesBehavior extends ModelBehavior {
 
function setup(&$model, $settings = array()) {
$default = array('field' => 'salt');
 
if (!isset($this->settings[$model->name])) {
$this->settings[$model->name] = $default;
}
 
$this->settings[$model->name] = array_merge($this->settings[$model->name], ife(is_array($settings), $settings, array()));
}
 
function beforeFind(&$model, $queryData) {
if(!empty($queryData['conditions'][$model->name.'.password']) && !empty($queryData['conditions'][$model->name.'.username'])) {
$user_id = $this->findSaltedUser($model, $queryData['conditions']);
if (!empty($user_id)) {
unset($queryData['conditions']);
$queryData['conditions'] = $user_id;
}
}
return $queryData;
}
 
function beforeSave(&$model) {
if (empty($this->id) && !empty($model->data[$model->name])) {
$data = &$model->data[$model->name];
$data['password'] = $this->generateSaltedPassword($data['password'], $data[$this->settings[$model->name]['field']]);
}
return parent::beforeSave(&$model);
}
 
function generateSaltedPassword($password = '', $saltString) {
if (!empty($password)) {
return Security::hash($password.$saltString, null, false);
}
}
 
function findSaltedUser(&$model, $fields = array()) {
if (!empty($fields)) {
$user_id = $model->query('SELECT `'.$model->name.'`.`id` as \'id\' FROM '.$model->table.' as '.$model->name.' WHERE `'.$model->name.'`.`username` = \''.$fields[$model->name.'.username'].'\' AND `'.$model->name.'`.`password` = SHA1(CONCAT(\''.$fields[$model->name.'.password'].'\', `'.$this->settings[$model->name]['field'].'`)) LIMIT 1');
if (!empty($user_id)) {
$fields[$model->name.'.id'] = $user_id[0][$model->name]['id'];
unset($fields[$model->name.'.password'], $fields[$model->name.'.username']);
}
}
return $fields;
}
 
function hashPasswords(&$data, $alias) {
if (isset($data[$alias]['password'])) {
$model->data = $data;
$model->data[$alias][$this->settings[$alias]['field']] = Security::hash(String::uuid(), null, true);
$model->data[$alias]['password'] = Security::hash($data[$alias]['password'], null, true);
return $model->data;
}
return $data;
}
 
}
 
?>

Now in your model you would do something like:

1
2
3
4
5
6
7
8
9
10
11
12
13
<?php
 
class Member extends AppModel {
 
var $actsAs = array('GreyTables');
 
function hashPasswords($data) {
return $this->Behaviors->GreyTables->hashPasswords($data, $this->alias);
}
 
}
 
?>

It’s a simple work around to get the behaviour to use the hashPasswords method automatically called. I am yet to do any extensive benchmarks on this code, and will have to include more settings in regards to how the salting is handled, and also the default username/password/email fields to be used to check the Auth. That will come soon. There is one flaw however, in that the conditions are constructed in two parts. The first part looks for the resalted username/password combo, then the second part uses the id found from that query and follows up with any other conditions to be used for the query. For example:
First query:

SELECT `Member`.`id` AS 'id' FROM members AS Member WHERE `Member`.`username` = 'testuser' AND `Member`.`password` = SHA1(CONCAT('1e71a44447c4e3ea05e8f81f031702f00d19c48e', `salt`)) LIMIT 1

Second query:

SELECT `Member`.`id`, `Member`.`username`, `Member`.`email`, `Member`.`password`, `Member`.`salt`, `Member`.`active`, `Member`.`created`, `Member`.`modified` FROM `members` AS `Member` WHERE `Member`.`active` = 1 AND `Member`.`id` = 52 LIMIT 1

I will be working to make the second query work as a read via the Id, as apposed to extra conditions.
More to come!