Skip to content

Commit 34f38bc

Browse files
committed
add preliminary discusion of reg bcrypt vs openssh's bcrypt
1 parent 2a0dd0f commit 34f38bc

File tree

1 file changed

+111
-0
lines changed

1 file changed

+111
-0
lines changed

docs/bcrypt.md

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
---
2+
id: bcrypt
3+
title: OpenSSH's bcrypt implementation
4+
---
5+
6+
OpenSSH encrypted private keys use a modified form of bcrypt for [password hashing](https://en.wikipedia.org/wiki/Key_derivation_function#Password_hashing). Because of these modifications neither PHP's [password_hash()](https://www.php.net/manual/en/function.password-hash.php) or [crypt()](https://www.php.net/manual/en/function.crypt.php) can be used. [OpenSSH's bcrypt_pbkdf.c](https://github.com/openssh/openssh-portable/blob/master/openbsd-compat/bcrypt_pbkdf.c) enumerates on some of these changes but not all of them.
7+
8+
To illustrate the differences we'll consider a number of different scenarios.
9+
10+
## Making phpseclib match `crypt()`
11+
12+
The following changes (notated using the [phpBB MOD Text Template](phpbb.md#actions)) are for [phpseclib 3.0.38's version of phpseclib/Crypt/Blowfish.php](https://github.com/phpseclib/phpseclib/blob/3.0.38/phpseclib/Crypt/Blowfish.php). They may or may not work on other versions.
13+
14+
```
15+
#
16+
#-----[ FIND ]------------------------------------------
17+
#
18+
private static function bcrypt_hash($sha2pass, $sha2salt)
19+
#
20+
#-----[ REPLACE WITH ]----------------------------------
21+
# this change is to that we can verify the changes
22+
#
23+
public static function bcrypt_hash($sha2pass, $sha2salt)
24+
#
25+
#-----[ FIND ]------------------------------------------
26+
#
27+
$cdata = array_values(unpack('N*', 'OxychromaticBlowfishSwatDynamite'));
28+
#
29+
#-----[ REPLACE WITH ]----------------------------------
30+
# OpenSSH's bcrypt_pbkdf.c documents this change
31+
#
32+
$cdata = array_values(unpack('N*', 'OrpheanBeholderScryDoubt'));
33+
#
34+
#-----[ FIND ]------------------------------------------
35+
#
36+
self::expand0state($sha2salt, $sbox, $p);
37+
self::expand0state($sha2pass, $sbox, $p);
38+
#
39+
#-----[ REPLACE WITH ]----------------------------------
40+
# this is an undocumented change; basically, the function calls are swapped
41+
#
42+
self::expand0state($sha2pass, $sbox, $p);
43+
self::expand0state($sha2salt, $sbox, $p);
44+
#
45+
#-----[ FIND ]------------------------------------------
46+
#
47+
for ($j = 0; $j < 8; $j += 2) { // count($cdata) == 8
48+
#
49+
#-----[ REPLACE WITH ]----------------------------------
50+
# this change follows as a natural consequence of the OrpheanBeholderScryDoubt change
51+
#
52+
for ($j = 0; $j < 6; $j += 2) { // count($cdata) == 6
53+
#
54+
#-----[ FIND ]------------------------------------------
55+
#
56+
return pack('L*', ...$cdata);
57+
#
58+
#-----[ REPLACE WITH ]----------------------------------
59+
# this is an undocumented change
60+
#
61+
return pack('N*', ...$cdata);
62+
```
63+
Even with these changes you'll still need to call `Blowfish::bcrypt_hash()` differently than you would `crypt()`. `password_hash()` won't do since "_as of PHP 8.0.0, an explicitly given salt is ignored_".
64+
65+
```
66+
$salt = '1234567812345678';
67+
68+
// quoting OpenSSH's bcrypt_pbkdf.c, "input password and salt are preprocessed with SHA512"
69+
// this means that we need to make them both 64 bytes long altho we'll expand the salt later
70+
$pass = 'aaaaaaaabbbbbbbbccccccccddddddddaaaaaaaabbbbbbbbccccccccdddddddd';
71+
72+
// we concatenate $salt to itself so that it's 64 bytes long
73+
// as wikipedia notes, the output is "the "base-64 encoding of the first 23 bytes of the computed 24 byte hash",
74+
// hence the substr(..., 0, -1) bit
75+
$hash = substr(Blowfish::bcrypt_hash($pass, $salt . $salt . $salt . $salt), 0, -1);
76+
77+
echo '$2a$06$' . encodeBase64($salt) . encodeBase64($hash) . "\n";
78+
79+
// we concatenate substr($pass, 0, 8) to $pass because that's what OpenSSH's bcrypt_pbkdf.c does
80+
//
81+
// the $2a$ part of the salt tells bcrypt to add a null byte to the end. as https://en.wikipedia.org/wiki/Bcrypt
82+
// notes you're supposed to "treat the password as cyclic" but if you're adding a null byte to the end you're
83+
// essentially doing "\0" . substr($pass, 0, 7) vs substr($pass, 0, 8)
84+
//
85+
// some bcrypt implementations support $2$, which doesn't specify whether or not a null byte ought to be tacked
86+
// onto the end or not, so of those that do support $2$ it's a tossup as to how it'd behave.
87+
//
88+
// $2x$ and $2y$ behave as $2a$ does w.r.t. the null byte. PHP doesn't support $2$
89+
//
90+
// the $06$ bit means that OpenSSH's implementation has a "cost" of 6
91+
echo crypt($pass . substr($pass, 0, 8), '$2a$06$' . encodeBase64($salt));
92+
93+
// encodeBase64() is used vs base64_encode() because crypt() uses it's own custom alphabet.
94+
function encodeBase64($input)
95+
{
96+
$old = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
97+
$new = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
98+
$input = base64_encode($input);
99+
$output = '';
100+
for ($i = 0; $i < strlen($input); $i++) {
101+
if ($input[$i] == '=') {
102+
break;
103+
}
104+
$pos = strpos($old, $input[$i]);
105+
$output.= $new[$pos];
106+
}
107+
108+
return $output;
109+
}
110+
```
111+
The two key takeaways from this are: (1) `Blowfish::bcrypt_hash()` expects 64 byte inputs whereas the normal bcrypt uses 8 byte salts and variable length passwords and (2) the last byte of `Blowfish::bcrypt_hash()` need to be removed.

0 commit comments

Comments
 (0)