Code Coverage
 
Classes and Traits
Functions and Methods
Lines
Total
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 10
CRAP
0.00% covered (danger)
0.00%
0 / 68
iaPwd
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 10
930.00
0.00% covered (danger)
0.00%
0 / 68
 __construct
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 4
 get_policy
n/a
0 / 0
1
n/a
0 / 0
 set_policy
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 2
 set_irregularSequences
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 2
 passwordPolicyCheck
0.00% covered (danger)
0.00%
0 / 1
210.00
0.00% covered (danger)
0.00%
0 / 23
 has_sequence
0.00% covered (danger)
0.00%
0 / 1
42.00
0.00% covered (danger)
0.00%
0 / 15
 get_rules
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 12
 passwordHash
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 1
 passwordVerify
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 1
 passwordNeedsRehash
0.00% covered (danger)
0.00%
0 / 1
2.00
0.00% covered (danger)
0.00%
0 / 1
 costRecomended
0.00% covered (danger)
0.00%
0 / 1
6.00
0.00% covered (danger)
0.00%
0 / 7
<?php
namespace ia\Lib;
use ia\Util\Str;
use URLify;
//@TODO
        // check max wrong attempts 30, block for x time? in caller
        // check vs last n pwds ($gsqlClass, $table, $pk)?
        // invalid list
// reading
    //https://www.networkworld.com/article/3199607/linux/dealing-with-nists-about-face-on-password-complexity.html
        //the maximum length for passwords be set to 64 characters min 8
        //passwords be checked against known bad passwords, banned lists, etc
        //passwords only be changed when forgotten
        //http://world.std.com/~reinhold/diceware.html
        //https://www.grc.com/haystack.htm
        //*** https://github.com/danielmiessler/SecLists
        //**** https://github.com/ircmaxell/password-policy
    // minLength: 0 } o 1
/**
 * iaPwd Check password vs characters policy, hashit and check hash vs password
 *
 */
class iaPwd {
    protected $options = ['cost' => 10];
    protected $algorithm = PASSWORD_DEFAULT;
    protected $policy = [
        'pwd_min_chars' => 12,
        'pwd_max_chars' => 60,
        'pwd_min_digitos' => 1,
        'pwd_min_minusculas' => 1,
        'pwd_min_mayusculas' => 1,
        'pwd_min_simbolos' => 1,
        'pwd_max_sequenciales' => 4,
    ];
    protected $irregularSequences = [
        'qwertyuiop',
        'asdfghijkl',
        'zxcvbnm',
        'poiuytrewq',
        'lkjhgfdsa',
        'mnbvcxz',
        '986532',
    ];
    /**
     * iaPwd::__construct()
     *
     * @param int $algorithm php's algorithm constant
     * @param integer $cost
     * @param array $policy
     */
    public function __construct($algorithm = PASSWORD_DEFAULT, $cost = 10, $policy = []) {
        $this->algorithm = $algorithm;
        $this->options['cost'] = $cost;
        $this->set_policy($policy);
    }
    /**
     * Policy for min/max characters, digits, lowercase, uppercase, symbols, sequences
     *
     * @return array
     */
    public function get_policy() {return $this->policy;}
    /** check password policy */
    /**
     * Set policy for min/max characters, digits, lowercase, uppercase, symbols, sequences
     *
     * @param array $policy one or more keys from
     *   'pwd_min_chars','pwd_max_chars', 'pwd_min_digitos', 'pwd_min_minusculas','pwd_min_mayusculas', 'pwd_min_simbolos', 'pwd_max_sequenciales'
     *
     */
    public function set_policy($policy) {
        $this->policy = array_merge($this->policy, $policy);
    }
    public function set_irregularSequences($irregularSequences) {
        $this->irregularSequences = $irregularSequences;
    }
    /**
     * Check the passwords complies with character's policy
     *
     * @param string $pwd
     * @return string error message or empty
     */
    public function passwordPolicyCheck($pwd) {
        if(empty($pwd)) {
            return 'Falto la contresaña';
        }
        $trim = Str::strim($pwd);
        if($trim !== $pwd) {
           return "No puede comenzar ni terminar con espacio, ni tener 2 espacios juntos";
        }
        $str = URLify::downcode($trim);
        if($str !== $trim) {
            return "No puede llevar acentos, dierecis, eñes, ni ¿ ¡";
        }
        $rules = $this->get_rules();
        foreach($rules as $i=>$r) {
            $n = preg_match_all($r['regexp'], $str, $matches);
            if(!empty($r['min']) && $n < $r['min']) {
                return $r['msg'];
            }
            if(!empty($r['max']) && $n > $r['max']) {
                return $r['msg'];
            }
            if($n > 0 && !empty($r['existe'])) {
                return $r['msg'];
            }
        }
        if($this->has_sequence($str, $this->policy['pwd_max_sequenciales'])) {
            return "No puede tener $this->policy[pwd_max_sequenciales] o más letras o números consecutivos";
        }
        foreach($this->irregularSequences as $s) {
            if(stripos($str, substr($s,0,$this->policy['pwd_min_chars']))!==false) {
                return "La sequencia de carácteres $s es inválida";
            }
        }
        return '';
    }
    /**
     * has_sequence()
     *
     * @param string $str
     * @param int $maxSecuenciales
     * @return bool
     */
    private function has_sequence($str, $maxSecuenciales) {
        $maxSecuenciales--;
        if($maxSecuenciales <= 0) {
            return false;
        }
        $seguidos = 0;
        $prev = 0;
        $len = strlen($str);
        for($i = 0; $i < $len; $i++) {
            $c = ord( strtolower( $str[$i] ) );
            if($c === $prev + 1 || $c === $prev - 1) {
                $seguidos++;
                if($seguidos >= $maxSecuenciales) {
                    return true;
                }
            } else {
                $seguidos = 0;
            }
            $prev = $c;
        }
        return false;
    }
    /**
     * Rules for passwords [regexp=>'to full fill', 'min'=>1, 'max'=>8, 'msg'=>'Explain rule']
     *
     * @return array
     */
    private function get_rules() {
        return [
            ['regexp'=>'/./',     'min'=>$this->policy['pwd_min_chars'], 'max'=>$this->policy['pwd_max_chars'],
                'msg'=>"al menos ".$this->policy['pwd_min_chars']." y máximo ".$this->policy['pwd_max_chars']." caracteres"],
            ['regexp'=>'/\\d/',   'min'=>$this->policy['pwd_min_digitos'],
                'msg'=>"al menos ".$this->policy['pwd_min_digitos']." número"],
            ['regexp'=>'/[a-z]/', 'min'=>$this->policy['pwd_min_minusculas'],
                'msg'=>"al menos ".$this->policy['pwd_min_minusculas']." minúscula"],
            ['regexp'=>'/[A-Z]/', 'min'=>$this->policy['pwd_min_mayusculas'],
                'msg'=>"al menos ".$this->policy['pwd_min_mayusculas']." mayúscula"],
            ['regexp'=>'/[^a-zA-Z\\d\\s]/', 'min'=>$this->policy['pwd_min_simbolos'],
                'msg'=>"al menos ".$this->policy['pwd_min_simbolos']." symbolo (! \" \' . , ; : ? = # $ % / ( ) - _ { } [ ] * + )"],
            ['regexp' => '/([a-zA-Z0-9])\1{'.($this->policy['pwd_max_sequenciales'] - 1).",}/", 'existe'=>true,
                'msg'=>"máximo caracteres ".$this->policy['pwd_max_sequenciales']." iguales seguidos"],
        ];
    }
    /** Hash password **/
    /**
     * Hash the password
     *
     * @param string $password
     * @return string
     */
    public function passwordHash($password) {
        return password_hash($password, $this->algorithm, $this->options);
    }
    /**
     * Check password matches hash
     *
     * @param string $password
     * @param string $hash
     * @return bool
     */
    public function passwordVerify($password, $hash) {
        return password_verify($password, $hash);
    }
    /**
     * Rehash the pasword's hash, for example on cost, algorithm change
     *
     * @param string $hash
     * @return string
     */
    public function passwordNeedsRehash($hash) {
      return  password_needs_rehash($hash , $this->algorithm, $this->options);
    }
    /**
     * Recomend best cost for time target in seconds
     *
     * @param float $timeTarget (seconds)
     * @return int recomended password cost
     */
    public function costRecomended($timeTarget = 0.1) {
        $cost = 8;
        do {
            $cost++;
            $start = microtime(true);
            password_hash("test", $this->algorithm, ["cost" => $cost]);
            $end = microtime(true);
        } while (($end - $start) < $timeTarget);
        return $cost;
    }
}