-
Notifications
You must be signed in to change notification settings - Fork 1
/
McfHash.php
170 lines (147 loc) · 5.18 KB
/
McfHash.php
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
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
<?php
/**
* (Binary) Modular Crypt Format Hash Encoding
*
* @package mcf-hash-encoder-php
* @link https://github.com/ademarre/mcf-hash-encoder-php
* @author Andre DeMarre
* @copyright 2013 Andre DeMarre
* @license http://opensource.org/licenses/MIT MIT
*/
/**
* Encoding and Decoding of (Binary) Modular Crypt Format hashes
*
* The McfHash class offers a compact binary serialization
* of hash digests which follow the Modular Crypt Format (MCF),
* a de facto notational convention used by the crypt(3) *nix
* function, and implemented in several programming languages,
* including PHP.
*
* Example:
* $2y$14$i5btSOiulHhaPHPbgNUGdObga/GC.AVG/y5HHY1ra7L0C9dpCaw8u
*
* For now, only the Bcrypt hash scheme is supported, but it is
* expandable to support hashes from other algorithms without
* affecting existing stored BMCF hashes.
*
* @see https://github.com/ademarre/binary-mcf BMCF Specification
* @see http://pythonhosted.org/passlib/modular_crypt_format.html
* @see http://en.wikipedia.org/wiki/Crypt_%28C%29
* @see http://php.net/crypt
*
* @package mcf-hash-encoder-php
*/
class McfHash
{
/**
* Bcrypt base-64 encoding alphabet
*/
const CHARS_BCRYPT = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
/**
* RFC 4648 base64 encoding alphabet
*/
const CHARS_BASE64 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
/**
* 3-bit scheme identifiers (in the three most significant bits)
*
* @var array
*/
protected $schemes = array(
// 0x00 => 'blank', // Reserved; perhaps for the implicit DES schemes
0x20 => '2',
0x40 => '2a',
0x60 => '2x',
0x80 => '2y',
// 0xA0 => '5', // Reserved
// 0xC0 => '6', // Reserved
// 0xE0 => 'other', // Reserved for overflow to next 5 bits
);
/**
* Decode an MCF hash into binary form (BMCF)
*
* @param string $hash MCF hash
* @return string Compact BMCF of $hash
*
* @throws InvalidArgumentException
* @throws RangeException
* @throws UnexpectedValueException
*/
public function decode($hash)
{
if (!is_string($hash)) throw new InvalidArgumentException('$hash must be a string');
if ($hash[0] != '$') throw new RangeException('Unsupported hash scheme'); // Possibly a DES hash
$parts = explode('$', $hash, 4);
$schemes = array_flip($this->schemes);
if (!isset($parts[2]) || !isset($schemes[$parts[1]])) {
throw new RangeException('Unsupported hash scheme');
}
// At this point we know we're working with a Bcrypt hash
if (strlen($parts[3]) != 53) {
throw new UnexpectedValueException('Invalid Bcrypt hash');
}
$scheme = $parts[1];
$cost = $parts[2];
if (strlen($cost) != 2 || !ctype_digit($cost)) {
throw new UnexpectedValueException('Invalid Bcrypt cost');
}
$headerOctet = $schemes[$scheme] | (intval($cost) & 0x1F);
$binaryHash = pack('C', $headerOctet);
// Decode the salt
$binaryHash .= $this->bcrypt64Decode(substr($parts[3], 0, 22));
// Decode the hash digest
$binaryHash .= $this->bcrypt64Decode(substr($parts[3], 22, 31));
return $binaryHash;
}
/**
* Encode a BMCF hash into textual notation (MCF)
*
* @param string $binaryHash BMCF hash
* @return string MCF $binaryHash
*
* @throws InvalidArgumentException
* @throws RangeException
* @throws UnexpectedValueException
*/
public function encode($binaryHash)
{
if (!is_string($binaryHash) || !isset($binaryHash[12])) {
throw new InvalidArgumentException('$binaryHash must be a string of at least 13 bytes');
}
$octets = unpack('C', $binaryHash[0]);
$headerOctet = array_shift($octets);
// Get the scheme identifier
$schemeId = $headerOctet & 0xE0;
if (!isset($this->schemes[$schemeId])) {
throw new RangeException('Unsupported hash scheme');
}
$scheme = $this->schemes[$schemeId];
// At this point we know we're working with a Bcrypt hash
if (strlen($binaryHash) != 40) {
throw new UnexpectedValueException('Invalid Bcrypt hash');
}
$cost = sprintf('%02u', $headerOctet - $schemeId);
$salt = $this->bcrypt64Encode(substr($binaryHash, 1, 16));
$digest = $this->bcrypt64Encode(substr($binaryHash, 17, 23));
return '$' . $scheme . '$' . $cost . '$' . $salt . $digest;
}
/**
* Encode Bcrypt base-64
*
* @param string $data The data to encode
* @return string Encoded data
*/
protected function bcrypt64Encode($data)
{
return strtr(rtrim(base64_encode($data), '='), self::CHARS_BASE64, self::CHARS_BCRYPT);
}
/**
* Decode Bcrypt base-64
*
* @param string $data The data to decode
* @return string Decoded data
*/
protected function bcrypt64Decode($data)
{
return base64_decode(strtr($data, self::CHARS_BCRYPT, self::CHARS_BASE64));
}
}