Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
49.43% covered (danger)
49.43%
43 / 87
25.00% covered (danger)
25.00%
2 / 8
CRAP
0.00% covered (danger)
0.00%
0 / 1
Password
49.43% covered (danger)
49.43%
43 / 87
25.00% covered (danger)
25.00%
2 / 8
164.46
0.00% covered (danger)
0.00%
0 / 1
 comparePasswordToHash
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
2.06
 encodePassword
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
1
 blowfishSalt
96.00% covered (success)
96.00%
24 / 25
0.00% covered (danger)
0.00%
0 / 1
4
 getBytes
0.00% covered (danger)
0.00%
0 / 33
0.00% covered (danger)
0.00%
0 / 1
182
 isBlowfishHash
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
6
 randomPassword
100.00% covered (success)
100.00%
8 / 8
100.00% covered (success)
100.00%
1 / 1
3
 hashEquals
46.15% covered (danger)
46.15%
6 / 13
0.00% covered (danger)
0.00%
0 / 1
11.62
 __construct
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
1<?php
2/**
3 * @section LICENSE
4 * This file is part of Wikimedia Slim application library
5 *
6 * Wikimedia Slim application library is free software: you can
7 * redistribute it and/or modify it under the terms of the GNU General Public
8 * License as published by the Free Software Foundation, either version 3 of
9 * the License, or (at your option) any later version.
10 *
11 * Wikimedia Slim application library is distributed in the hope that it
12 * will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty
13 * of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
14 * General Public License for more details.
15 *
16 * You should have received a copy of the GNU General Public License along
17 * with Wikimedia Grants Review application.  If not, see
18 * <http://www.gnu.org/licenses/>.
19 *
20 * @file
21 * @copyright © 2015 Bryan Davis, Wikimedia Foundation and contributors.
22 */
23
24namespace Wikimedia\Slimapp\Auth;
25
26use InvalidArgumentException;
27
28/**
29 * Password management utility.
30 *
31 * @author Bryan Davis <bd808@wikimedia.org>
32 * @copyright © 2015 Bryan Davis, Wikimedia Foundation and contributors.
33 */
34class Password {
35
36    /**
37     * Blowfish hashing salt prefix for crypt.
38     * @var string BLOWFISH_PREFIX
39     */
40    private const BLOWFISH_PREFIX = '$2y$';
41
42    /**
43     * Compare a plain text string to a stored password hash.
44     *
45     * @param string $plainText Password to check
46     * @param string $hash Stored hash to compare with
47     * @return bool True if plain text matches hash, false otherwise
48     */
49    public static function comparePasswordToHash( $plainText, $hash ) {
50        if ( self::isBlowfishHash( $hash ) ) {
51            $check = crypt( $plainText, $hash );
52
53        } else {
54            // horrible unsalted md5 that legacy app used for passwords
55            $check = md5( $plainText );
56        }
57
58        return self::hashEquals( $hash, $check );
59    }
60
61    /**
62     * Encode a password for database storage.
63     *
64     * Do not use the direct output of this function for comparison with stored
65     * values. Modern password hashes use unique salts per encoding and will not
66     * be directly comparable. Use the comparePasswordToHash() function for
67     * validation instead.
68     *
69     * @param string $plainText Password in plain text
70     * @return string Encoded password
71     */
72    public static function encodePassword( $plainText ) {
73        $salt = self::blowfishSalt();
74        return crypt( $plainText, $salt );
75    }
76
77    /**
78     * Generate a blowfish salt specification.
79     *
80     * @param int $cost Cost factor
81     * @return string Blowfish salt
82     */
83    public static function blowfishSalt( $cost = 8 ) {
84        // encoding algorithm from http://www.openwall.com/phpass/
85        $itoa = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
86        if ( $cost < 4 || $cost > 31 ) {
87            $cost = 8;
88        }
89        $random = self::getBytes( 16 );
90
91        $output = self::BLOWFISH_PREFIX;
92        $output .= chr( ord( '0' ) + (int)( $cost / 10 ) );
93        $output .= chr( ord( '0' ) + $cost % 10 );
94        $output .= '$';
95
96        $i = 0;
97        do {
98            $c1 = ord( $random[$i++] );
99            $output .= $itoa[$c1 >> 2];
100            $c1 = ( $c1 & 0x03 ) << 4;
101            if ( $i >= 16 ) {
102                $output .= $itoa[$c1];
103                break;
104            }
105
106            $c2 = ord( $random[$i++] );
107            $c1 |= $c2 >> 4;
108            $output .= $itoa[$c1];
109            $c1 = ( $c2 & 0x0f ) << 2;
110
111            $c2 = ord( $random[$i++] );
112            $c1 |= $c2 >> 6;
113            $output .= $itoa[$c1];
114            $output .= $itoa[$c2 & 0x3f];
115        } while ( 1 );
116
117        return $output;
118    }
119
120    /**
121     * Get N high entropy random bytes.
122     *
123     * @param int $count Number of bytes to generate
124     * @param bool $allowWeak Allow weak entropy sources
125     * @return string String of random bytes
126     * @throws InvalidArgumentException if $allowWeak is false and no high
127     * entropy sources of random data can be found
128     */
129    public static function getBytes( $count, $allowWeak = false ) {
130        if ( function_exists( 'random_bytes' ) ) {
131            $bytes = random_bytes( $count );
132            if ( strlen( $bytes ) === $count ) {
133                return $bytes;
134            }
135        }
136
137        if ( function_exists( 'openssl_random_pseudo_bytes' ) ) {
138            $strong = null;
139            $bytes = openssl_random_pseudo_bytes( $count, $strong );
140
141            if ( $strong && strlen( $bytes ) === $count ) {
142                return $bytes;
143            }
144        }
145
146        if ( is_readable( '/dev/urandom' ) ) {
147            // @codingStandardsIgnoreStart : Silencing errors is discouraged
148            $fh = @fopen( '/dev/urandom', 'rb' );
149            // @codingStandardsIgnoreEnd
150            if ( $fh !== false ) {
151                $bytes = '';
152                $have = 0;
153                while ( $have < $count ) {
154                    $bytes .= fread( $fh, $count - $have );
155                    $have = strlen( $bytes );
156                }
157                fclose( $fh );
158
159                if ( strlen( $bytes ) === $count ) {
160                    return $bytes;
161                }
162            }
163        }
164
165        if ( $allowWeak !== true ) {
166            throw new InvalidArgumentException(
167                'No high entropy source of random data found and ' .
168                'weak sources disallowed in function call'
169            );
170        }
171
172        // create a high entropy seed value
173        $seed = microtime() . uniqid( '', true );
174        if ( function_exists( 'getmypid' ) ) {
175            $seed .= getmypid();
176        }
177
178        $bytes = '';
179        for ( $i = 0; $i < $count; $i += 16 ) {
180            $seed = md5( microtime() . $seed );
181            $bytes .= pack( 'H*', md5( $seed ) );
182        }
183
184        return substr( $bytes, 0, $count );
185    }
186
187    /**
188     * Check a salt specification to see if it is a blowfish crypt value.
189     *
190     * @param string $hash Hash to check
191     * @return bool True if blowfish, false otherwise.
192     */
193    public static function isBlowfishHash( $hash ) {
194        return strlen( $hash ) === 60 && strpos( $hash, self::BLOWFISH_PREFIX ) === 0;
195    }
196
197    // @codingStandardsIgnoreStart : Line exceeds 100 characters
198    private const CHARSET_PRINTABLE = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~';
199    // @codingStandardsIgnoreEnd
200    private const CHARSET_UPPER = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
201    private const CHARSET_LOWER = 'abcdefghijklmnopqrstuvwxyz';
202    private const CHARSET_DIGIT = '0123456789';
203    private const CHARSET_ALPHANUM = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
204    private const CHARSET_SYMBOL = '!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~';
205
206    /**
207     * Generate a random password.
208     *
209     * Note: This is not the world's greatest password generation algorithm. It
210     * uses a selection technique that has some bias based on modulo
211     * arithmetic. If you need a truely random password you'll need to look
212     * somewhere else. If you just need a temporary password to email to a user
213     * who will promptly log in and change their password to 'god', this should
214     * be good enough.
215     *
216     * @param int $len Length of password desired
217     * @param string $cs Symbol set to select password characters from
218     * @return string Password
219     */
220    public static function randomPassword( $len, $cs = null ) {
221        if ( $cs === null ) {
222            $cs = self::CHARSET_PRINTABLE;
223        }
224        $csLen = strlen( $cs );
225
226        $random = self::getBytes( $len, true );
227        $password = '';
228
229        foreach ( range( 0, $len - 1 ) as $i ) {
230            $password .= $cs[ ord( $random[$i] ) % $csLen ];
231        }
232
233        return $password;
234    }
235
236    /**
237     * Check whether a user-provided string is equal to a fixed-length secret
238     * string without revealing bytes of the secret string through timing
239     * differences.
240     *
241     * Implementation for PHP deployments which do not natively have
242     * hash_equals taken from MediaWiki's hash_equals() polyfill function.
243     *
244     * @param string $known Fixed-length secret string to compare against
245     * @param string $input User-provided string
246     * @return bool True if the strings are the same, false otherwise
247     */
248    public static function hashEquals( $known, $input ) {
249        if ( !is_string( $known ) ) {
250            return false;
251        }
252        if ( !is_string( $input ) ) {
253            return false;
254        }
255
256        if ( function_exists( 'hash_equals' ) ) {
257            return hash_equals( $known, $input );
258
259        }
260
261        // hash_equals() polyfill taken from MediaWiki
262        $len = strlen( $known );
263        if ( $len !== strlen( $input ) ) {
264            return false;
265        }
266
267        $result = 0;
268        for ( $i = 0; $i < $len; $i++ ) {
269            $result |= ord( $known[$i] ) ^ ord( $input[$i] );
270        }
271
272        return $result === 0;
273    }
274
275    /**
276     * Construction of utility class is not allowed.
277     */
278    private function __construct() {
279        // no-op
280    }
281
282}