1<?php
2#==============================================================================
3# LTB Self Service Password
4#
5# Copyright (C) 2009 Clement OUDOT
6# Copyright (C) 2009 LTB-project.org
7#
8# This program is free software; you can redistribute it and/or
9# modify it under the terms of the GNU General Public License
10# as published by the Free Software Foundation; either version 2
11# of the License, or (at your option) any later version.
12#
13# This program is distributed in the hope that it will be useful,
14# but WITHOUT ANY WARRANTY; without even the implied warranty of
15# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
16# GNU General Public License for more details.
17#
18# GPL License: http://www.gnu.org/licenses/gpl.txt
19#
20#==============================================================================
21
22# Create SSHA password
23function make_ssha_password($password) {
24    $salt = random_bytes(4);
25    $hash = "{SSHA}" . base64_encode(pack("H*", sha1($password . $salt)) . $salt);
26    return $hash;
27}
28
29# Create SSHA256 password
30function make_ssha256_password($password) {
31    $salt = random_bytes(4);
32    $hash = "{SSHA256}" . base64_encode(pack("H*", hash('sha256', $password . $salt)) . $salt);
33    return $hash;
34}
35
36# Create SSHA384 password
37function make_ssha384_password($password) {
38    $salt = random_bytes(4);
39    $hash = "{SSHA384}" . base64_encode(pack("H*", hash('sha384', $password . $salt)) . $salt);
40    return $hash;
41}
42
43# Create SSHA512 password
44function make_ssha512_password($password) {
45    $salt = random_bytes(4);
46    $hash = "{SSHA512}" . base64_encode(pack("H*", hash('sha512', $password . $salt)) . $salt);
47    return $hash;
48}
49
50# Create SHA password
51function make_sha_password($password) {
52    $hash = "{SHA}" . base64_encode(pack("H*", sha1($password)));
53    return $hash;
54}
55
56# Create SHA256 password
57function make_sha256_password($password) {
58    $hash = "{SHA256}" . base64_encode(pack("H*", hash('sha256', $password)));
59    return $hash;
60}
61
62# Create SHA384 password
63function make_sha384_password($password) {
64    $hash = "{SHA384}" . base64_encode(pack("H*", hash('sha384', $password)));
65    return $hash;
66}
67
68# Create SHA512 password
69function make_sha512_password($password) {
70    $hash = "{SHA512}" . base64_encode(pack("H*", hash('sha512', $password)));
71    return $hash;
72}
73
74# Create SMD5 password
75function make_smd5_password($password) {
76    $salt = random_bytes(4);
77    $hash = "{SMD5}" . base64_encode(pack("H*", md5($password . $salt)) . $salt);
78    return $hash;
79}
80
81# Create MD5 password
82function make_md5_password($password) {
83    $hash = "{MD5}" . base64_encode(pack("H*", md5($password)));
84    return $hash;
85}
86
87# Create CRYPT password
88function make_crypt_password($password, $hash_options) {
89
90    $salt_length = 2;
91    if ( isset($hash_options['crypt_salt_length']) ) {
92        $salt_length = $hash_options['crypt_salt_length'];
93    }
94
95    // Generate salt
96    $possible = '0123456789'.
97		'abcdefghijklmnopqrstuvwxyz'.
98		'ABCDEFGHIJKLMNOPQRSTUVWXYZ'.
99		'./';
100    $salt = "";
101
102    while( strlen( $salt ) < $salt_length ) {
103        $salt .= substr( $possible, random_int( 0, strlen( $possible ) - 1 ), 1 );
104    }
105
106    if ( isset($hash_options['crypt_salt_prefix']) ) {
107        $salt = $hash_options['crypt_salt_prefix'] . $salt;
108    }
109
110    $hash = '{CRYPT}' . crypt( $password,  $salt);
111    return $hash;
112}
113
114# Create MD4 password (Microsoft NT password format)
115function make_md4_password($password) {
116    if (function_exists('hash')) {
117        $hash = strtoupper( hash( "md4", iconv( "UTF-8", "UTF-16LE", $password ) ) );
118    } else {
119        $hash = strtoupper( bin2hex( mhash( MHASH_MD4, iconv( "UTF-8", "UTF-16LE", $password ) ) ) );
120    }
121    return $hash;
122}
123
124# Create AD password (Microsoft Active Directory password format)
125function make_ad_password($password) {
126    $password = "\"" . $password . "\"";
127    $adpassword = mb_convert_encoding($password, "UTF-16LE", "UTF-8");
128    return $adpassword;
129}
130
131# Generate SMS token
132function generate_sms_token( $sms_token_length ) {
133    $Range=explode(',','48-57');
134    $NumRanges=count($Range);
135    $smstoken='';
136    for ($i = 1; $i <= $sms_token_length; $i++){
137        $r=random_int(0,$NumRanges-1);
138        list($min,$max)=explode('-',$Range[$r]);
139        $smstoken.=chr(random_int($min,$max));
140    }
141    return $smstoken;
142}
143
144# Get message criticity
145function get_criticity( $msg ) {
146
147    if ( preg_match( "/nophpldap|phpupgraderequired|nophpmhash|nokeyphrase|ldaperror|nomatch|badcredentials|passworderror|tooshort|toobig|minlower|minupper|mindigit|minspecial|forbiddenchars|sameasold|answermoderror|answernomatch|mailnomatch|tokennotsent|tokennotvalid|notcomplex|smsnonumber|smscrypttokensrequired|nophpmbstring|nophpxml|smsnotsent|sameaslogin|pwned|sshkeyerror|specialatends|forbiddenwords|forbiddenldapfields|diffminchars|badquality|tooyoung|inhistory|throttle/" , $msg ) ) {
148    return "danger";
149    }
150
151    if ( preg_match( "/(login|oldpassword|newpassword|confirmpassword|answer|question|password|mail|token|sshkey|captcha)required|badcaptcha|tokenattempts|checkdatabeforesubmit/" , $msg ) ) {
152        return "warning";
153    }
154
155    return "success";
156}
157
158# Get FontAwesome class icon
159function get_fa_class( $msg) {
160
161    $criticity = get_criticity( $msg );
162
163    if ( $criticity === "danger" ) { return "fa-exclamation-circle"; }
164    if ( $criticity === "warning" ) { return "fa-exclamation-triangle"; }
165    if ( $criticity === "success" ) { return "fa-check-square"; }
166
167}
168
169# Display policy bloc
170# @return HTML code
171function show_policy( $messages, $pwd_policy_config, $result ) {
172    extract( $pwd_policy_config );
173
174    # Should we display it?
175    if ( !$pwd_show_policy or $pwd_show_policy === "never" ) { return; }
176    if ( $pwd_show_policy === "onerror" ) {
177        if ( !preg_match( "/tooshort|toobig|minlower|minupper|mindigit|minspecial|forbiddenchars|sameasold|notcomplex|sameaslogin|pwned||specialatendsforbiddenwords|forbiddenldapfields/" , $result) ) { return; }
178    }
179
180    # Display bloc
181    echo "<div class=\"help alert alert-warning\">\n";
182    echo "<p>".$messages["policy"]."</p>\n";
183    echo "<ul>\n";
184    if ( $pwd_min_length      ) { echo "<li>".$messages["policyminlength"]      ." $pwd_min_length</li>\n"; }
185    if ( $pwd_max_length      ) { echo "<li>".$messages["policymaxlength"]      ." $pwd_max_length</li>\n"; }
186    if ( $pwd_min_lower       ) { echo "<li>".$messages["policyminlower"]       ." $pwd_min_lower</li>\n"; }
187    if ( $pwd_min_upper       ) { echo "<li>".$messages["policyminupper"]       ." $pwd_min_upper</li>\n"; }
188    if ( $pwd_min_digit       ) { echo "<li>".$messages["policymindigit"]       ." $pwd_min_digit</li>\n"; }
189    if ( $pwd_min_special     ) { echo "<li>".$messages["policyminspecial"]     ." $pwd_min_special</li>\n"; }
190    if ( $pwd_complexity      ) { echo "<li>".$messages["policycomplex"]        ." $pwd_complexity</li>\n"; }
191    if ( $pwd_forbidden_chars ) { echo "<li>".$messages["policyforbiddenchars"] ." $pwd_forbidden_chars</li>\n"; }
192    if ( $pwd_no_reuse        ) { echo "<li>".$messages["policynoreuse"]                                 ."</li>\n"; }
193    if ( $pwd_diff_last_min_chars ) { echo "<li>".$messages['policydiffminchars']." $pwd_diff_last_min_chars</li>\n"; }
194    if ( $pwd_diff_login      ) { echo "<li>".$messages["policydifflogin"]                               ."</li>\n"; }
195    if ( $use_pwnedpasswords  ) { echo "<li>".$messages["policypwned"]                               ."</li>\n"; }
196    if ( $pwd_no_special_at_ends  ) { echo "<li>".$messages["policyspecialatends"] ."</li>\n"; }
197    if ( !empty($pwd_forbidden_words)) { echo "<li>".$messages["policyforbiddenwords"] ." " . implode(', ', $pwd_forbidden_words) ."</li>\n"; }
198    if ( !empty($pwd_forbidden_ldap_fields)) {
199      $pwd_forbidden_ldap_fields = array_map(
200        function($field) use ($messages) {
201          if(empty($messages['ldap_' . $field])) return $field;
202          return $messages['ldap_' . $field];
203        }, $pwd_forbidden_ldap_fields);
204      echo "<li>".$messages["policyforbiddenldapfields"] ." " . implode(', ', $pwd_forbidden_ldap_fields) ."</li>\n";
205    }
206    echo "</ul>\n";
207    echo "</div>\n";
208}
209
210# Check password strength
211# @return result code
212function check_password_strength( $password, $oldpassword, $pwd_policy_config, $login, $entry ) {
213    extract( $pwd_policy_config );
214
215    $result = "";
216
217    $length = strlen(utf8_decode($password));
218    preg_match_all("/[a-z]/", $password, $lower_res);
219    $lower = count( $lower_res[0] );
220    preg_match_all("/[A-Z]/", $password, $upper_res);
221    $upper = count( $upper_res[0] );
222    preg_match_all("/[0-9]/", $password, $digit_res);
223    $digit = count( $digit_res[0] );
224
225    $special = 0;
226    $special_at_ends = false;
227    if ( isset($pwd_special_chars) && !empty($pwd_special_chars) ) {
228        preg_match_all("/[$pwd_special_chars]/", $password, $special_res);
229        $special = count( $special_res[0] );
230        if ( $pwd_no_special_at_ends ) {
231          $special_at_ends = preg_match("/(^[$pwd_special_chars]|[$pwd_special_chars]$)/", $password, $special_res);
232        }
233    }
234
235    $forbidden = 0;
236    if ( isset($pwd_forbidden_chars) && !empty($pwd_forbidden_chars) ) {
237        preg_match_all("/[$pwd_forbidden_chars]/", $password, $forbidden_res);
238        $forbidden = count( $forbidden_res[0] );
239    }
240
241    # Complexity: checks for lower, upper, special, digits
242    if ( $pwd_complexity ) {
243        $complex = 0;
244        if ( $special > 0 ) { $complex++; }
245        if ( $digit > 0 ) { $complex++; }
246        if ( $lower > 0 ) { $complex++; }
247        if ( $upper > 0 ) { $complex++; }
248        if ( $complex < $pwd_complexity ) { $result="notcomplex"; }
249    }
250
251    # Minimal lenght
252    if ( $pwd_min_length and $length < $pwd_min_length ) { $result="tooshort"; }
253
254    # Maximal lenght
255    if ( $pwd_max_length and $length > $pwd_max_length ) { $result="toobig"; }
256
257    # Minimal lower chars
258    if ( $pwd_min_lower and $lower < $pwd_min_lower ) { $result="minlower"; }
259
260    # Minimal upper chars
261    if ( $pwd_min_upper and $upper < $pwd_min_upper ) { $result="minupper"; }
262
263    # Minimal digit chars
264    if ( $pwd_min_digit and $digit < $pwd_min_digit ) { $result="mindigit"; }
265
266    # Minimal special chars
267    if ( $pwd_min_special and $special < $pwd_min_special ) { $result="minspecial"; }
268
269    # Forbidden chars
270    if ( $forbidden > 0 ) { $result="forbiddenchars"; }
271
272    # Special chars at beginning or end
273    if ( $special_at_ends > 0 && $special == 1 ) { $result="specialatends"; }
274
275    # Same as old password?
276    if ( $pwd_no_reuse and $password === $oldpassword ) { $result="sameasold"; }
277
278    # Same as login?
279    if ( $pwd_diff_login and $password === $login ) { $result="sameaslogin"; }
280
281    if ( $pwd_diff_last_min_chars > 0 ) {
282	$similarities = similar_text($oldpassword, $password);
283	$check_len    = strlen($oldpassword) < strlen($password) ? strlen($oldpassword) : strlen($password);
284	$new_chars    = $check_len - $similarities;
285	if ($new_chars <= $pwd_diff_last_min_chars) { $result = "diffminchars"; }
286    }
287
288    # Contains forbidden words?
289    if ( !empty($pwd_forbidden_words) ) {
290      foreach( $pwd_forbidden_words as $disallowed ) {
291        if( stripos($password, $disallowed) !== false ) {
292          $result="forbiddenwords";
293	  break;
294	}
295      }
296    }
297
298    # Contains values from forbidden ldap fields?
299    if( !empty($pwd_forbidden_ldap_fields) ) {
300      foreach( $pwd_forbidden_ldap_fields as $field ) {
301        $values = $entry[$field];
302        if(!is_array($entry[$field])) {
303          $values = array($entry[$field]);
304        }
305        foreach($values as $key => $value) {
306          if($key === 'count') continue;
307          if(stripos($password, $value) !== false) {
308            $result = "forbiddenldapfields";
309            break 2;
310          }
311        }
312      }
313    }
314
315    # pwned?
316    if ($use_pwnedpasswords) {
317        $pwned_passwords = new PwnedPasswords\PwnedPasswords;
318        $insecure = $pwned_passwords->isInsecure($password);
319        if($insecure) { $result="pwned"; }
320    }
321
322    return $result;
323}
324
325# Change password
326# @return result code
327function change_password( $ldap, $dn, $password, $ad_mode, $ad_options, $samba_mode, $samba_options, $shadow_options, $hash, $hash_options, $who_change_password, $oldpassword, $use_exop_passwd, $use_ppolicy_control ) {
328
329    $result = "";
330    $error_code = "";
331    $error_msg = "";
332    $ppolicy_error_code = "";
333
334    $time = time();
335
336    # Set Samba password value
337    if ( $samba_mode ) {
338        $userdata["sambaNTPassword"] = make_md4_password($password);
339        $userdata["sambaPwdLastSet"] = $time;
340        if ( isset($samba_options['min_age']) && $samba_options['min_age'] > 0 ) {
341             $userdata["sambaPwdCanChange"] = $time + ( $samba_options['min_age'] * 86400 );
342        }
343        if ( isset($samba_options['max_age']) && $samba_options['max_age'] > 0 ) {
344             $userdata["sambaPwdMustChange"] = $time + ( $samba_options['max_age'] * 86400 );
345        }
346        if ( isset($samba_options['expire_days']) && $samba_options['expire_days'] > 0 ) {
347             $userdata["sambaKickoffTime"] = $time + ( $samba_options['expire_days'] * 86400 );
348        }
349    }
350
351    # Get hash type if hash is set to auto
352    if ( !$ad_mode && $hash == "auto" ) {
353        $search_userpassword = ldap_read( $ldap, $dn, "(objectClass=*)", array("userPassword") );
354        if ( $search_userpassword ) {
355            $userpassword = ldap_get_values($ldap, ldap_first_entry($ldap,$search_userpassword), "userPassword");
356            if ( isset($userpassword) ) {
357                if ( preg_match( '/^\{(\w+)\}/', $userpassword[0], $matches ) ) {
358                    $hash = strtoupper($matches[1]);
359                }
360            }
361        }
362    }
363
364    # Transform password value
365    if ( $ad_mode ) {
366        $password = make_ad_password($password);
367    } elseif (!$use_exop_passwd) {
368        # Hash password if needed
369        if ( $hash == "SSHA" ) {
370            $password = make_ssha_password($password);
371        }
372        if ( $hash == "SSHA256" ) {
373            $password = make_ssha256_password($password);
374        }
375        if ( $hash == "SSHA384" ) {
376            $password = make_ssha384_password($password);
377        }
378        if ( $hash == "SSHA512" ) {
379            $password = make_ssha512_password($password);
380        }
381        if ( $hash == "SHA" ) {
382            $password = make_sha_password($password);
383        }
384        if ( $hash == "SHA256" ) {
385            $password = make_sha256_password($password);
386        }
387        if ( $hash == "SHA384" ) {
388            $password = make_sha384_password($password);
389        }
390        if ( $hash == "SHA512" ) {
391            $password = make_sha512_password($password);
392        }
393        if ( $hash == "SMD5" ) {
394            $password = make_smd5_password($password);
395        }
396        if ( $hash == "MD5" ) {
397            $password = make_md5_password($password);
398        }
399        if ( $hash == "CRYPT" ) {
400            $password = make_crypt_password($password, $hash_options);
401        }
402    }
403
404    # Set password value
405    if ( $ad_mode ) {
406        $userdata["unicodePwd"] = $password;
407        if ( $ad_options['force_unlock'] ) {
408            $userdata["lockoutTime"] = 0;
409        }
410        if ( $ad_options['force_pwd_change'] ) {
411            $userdata["pwdLastSet"] = 0;
412        }
413    }
414
415    # Shadow options
416    if ( $shadow_options['update_shadowLastChange'] ) {
417        $userdata["shadowLastChange"] = floor($time / 86400);
418    }
419
420    if ( $shadow_options['update_shadowExpire'] ) {
421        if ( $shadow_options['shadow_expire_days'] > 0) {
422          $userdata["shadowExpire"] = floor(($time / 86400) + $shadow_options['shadow_expire_days']);
423        } else {
424          $userdata["shadowExpire"] = $shadow_options['shadow_expire_days'];
425        }
426    }
427
428    # Commit modification on directory
429
430    # Special case: AD mode with password changed as user
431    if ( $ad_mode and $who_change_password === "user" ) {
432        # The AD password change procedure is modifying the attribute unicodePwd by
433        # first deleting unicodePwd with the old password and them adding it with the
434        # the new password
435        $oldpassword = make_ad_password($oldpassword);
436
437        $modifications = array(
438            array(
439                "attrib" => "unicodePwd",
440                "modtype" => LDAP_MODIFY_BATCH_REMOVE,
441                "values" => array($oldpassword),
442            ),
443            array(
444                "attrib" => "unicodePwd",
445                "modtype" => LDAP_MODIFY_BATCH_ADD,
446                "values" => array($password),
447            ),
448        );
449
450        $bmod = ldap_modify_batch($ldap, $dn, $modifications);
451        $error_code = ldap_errno($ldap);
452        $error_msg = ldap_error($ldap);
453    } elseif ($use_exop_passwd) {
454        $exop_passwd = FALSE;
455        if ( $use_ppolicy_control ) {
456            $ctrls = array();
457            $exop_passwd = ldap_exop_passwd($ldap, $dn, $oldpassword, $password, $ctrls);
458            $error_code = ldap_errno($ldap);
459            $error_msg = ldap_error($ldap);
460            if (!$exop_passwd) {
461                if (isset($ctrls[LDAP_CONTROL_PASSWORDPOLICYRESPONSE])) {
462                    $value = $ctrls[LDAP_CONTROL_PASSWORDPOLICYRESPONSE]['value'];
463                    if (isset($value['error'])) {
464                        $ppolicy_error_code = $value['error'];
465                        error_log("LDAP - Ppolicy error code: $ppolicy_error_code");
466                    }
467                }
468            }
469        } else {
470            $exop_passwd = ldap_exop_passwd($ldap, $dn, $oldpassword, $password);
471            $error_code = ldap_errno($ldap);
472            $error_msg = ldap_error($ldap);
473        }
474        if ($exop_passwd === TRUE) {
475            # If password change works update other data
476            if (!empty($userdata)) {
477                ldap_mod_replace($ldap, $dn, $userdata);
478                $error_code = ldap_errno($ldap);
479                $error_msg = ldap_error($ldap);
480            }
481        }
482    } else {
483        # Else just replace with new password
484        if (!$ad_mode) {
485            $userdata["userPassword"] = $password;
486        }
487	if ( $use_ppolicy_control ) {
488            $ppolicy_replace = ldap_mod_replace_ext($ldap, $dn, $userdata, [['oid' => LDAP_CONTROL_PASSWORDPOLICYREQUEST]]);
489	    if (ldap_parse_result($ldap, $ppolicy_replace, $error_code, $matcheddn, $error_msg, $referrals, $ctrls)) {
490                if (isset($ctrls[LDAP_CONTROL_PASSWORDPOLICYRESPONSE])) {
491                    $value = $ctrls[LDAP_CONTROL_PASSWORDPOLICYRESPONSE]['value'];
492		    if (isset($value['error'])) {
493			$ppolicy_error_code = $value['error'];
494                        error_log("LDAP - Ppolicy error code: $ppolicy_error_code");
495                    }
496                }
497	    }
498	} else {
499            ldap_mod_replace($ldap, $dn, $userdata);
500            $error_code = ldap_errno($ldap);
501            $error_msg = ldap_error($ldap);
502        }
503    }
504
505    if ( !isset($error_code) ) {
506        $result = "ldaperror";
507    } elseif ( $error_code > 0 ) {
508        $result = "passworderror";
509	error_log("LDAP - Modify password error $error_code ($error_msg)");
510	if ( $ppolicy_error_code === 5 ) { $result = "badquality"; }
511	if ( $ppolicy_error_code === 6 ) { $result = "tooshort"; }
512	if ( $ppolicy_error_code === 7 ) { $result = "tooyoung"; }
513	if ( $ppolicy_error_code === 8 ) { $result = "inhistory"; }
514    } else {
515        $result = "passwordchanged";
516    }
517
518    return $result;
519}
520
521
522# Change sshPublicKey attribute
523# @return result code
524function change_sshkey( $ldap, $dn, $attribute, $sshkey ) {
525
526    $result = "";
527
528    $userdata[$attribute] = $sshkey;
529
530    # Commit modification on directory
531    $replace = ldap_mod_replace($ldap, $dn, $userdata);
532
533    $errno = ldap_errno($ldap);
534
535    if ( $errno ) {
536        $result = "sshkeyerror";
537        error_log("LDAP - Modify $attribute error $errno (".ldap_error($ldap).")");
538    } else {
539        $result = "sshkeychanged";
540    }
541
542    return $result;
543}
544
545
546/* @function encrypt(string $data)
547 * Encrypt a data
548 * @param string $data Data to encrypt
549 * @param string $keyphrase Password for encryption
550 * @return string Encrypted data, base64 encoded
551 */
552function encrypt($data, $keyphrase) {
553    return base64_encode(\Defuse\Crypto\Crypto::encryptWithPassword($data, $keyphrase, true));
554}
555
556/* @function decrypt(string $data)
557 * Decrypt a data
558 * @param string $data Encrypted data, base64 encoded
559 * @param string $keyphrase Password for decryption
560 * @return string Decrypted data
561 */
562function decrypt($data, $keyphrase) {
563    try {
564        return \Defuse\Crypto\Crypto::decryptWithPassword(base64_decode($data), $keyphrase, true);
565    } catch (\Defuse\Crypto\Exception\CryptoException $e) {
566        error_log("crypto: decryption error " . $e->getMessage());
567        return '';
568    }
569}
570
571
572/* @function string str_putcsv(array $fields[, string $delimiter = ','[, string $enclosure = '"'[, string $escape_char = '\\']]])
573 * Convert array to CSV line. Based on https://gist.github.com/johanmeiring/2894568 and https://bugs.php.net/bug.php?id=64183#1506521511
574 * Wrapped in `if(!function_exists(...` in case it gets added to PHP.
575 * Also see https://www.php.net/manual/en/function.fgetcsv.php and related
576 * @param string $fields An array of strings
577 * @param string $delimiter field delimiter (one character only)
578 * @param string $enclosure field enclosure (one character only)
579 * @param string $escape_char escape character (at most one character) - empty string ("") disables escape mechanism
580 * @return string fields in CSV format
581 */
582if(!function_exists('str_putcsv'))
583{
584    function str_putcsv($fields, $delimiter = ',', $enclosure = '"', $escape_char = '\\')
585    {
586        $fp = fopen('php://temp', 'r+');
587        fputcsv($fp, $fields, $delimiter, $enclosure, $escape_char);
588        rewind($fp);
589        $data = stream_get_contents($fp);
590        fclose($fp);
591        return rtrim($data, "\n");
592    }
593}
594
595
596/* @function boolean send_mail(PHPMailer $mailer, string $mail, string $mail_from, string $subject, string $body, array $data)
597 * Send a mail, replace strings in body
598 * @param mailer PHPMailer object
599 * @param mail Destination
600 * @param mail_from Sender
601 * @param subject Subject
602 * @param body Body
603 * @param data Data for string replacement
604 * @return result
605 */
606function send_mail($mailer, $mail, $mail_from, $mail_from_name, $subject, $body, $data) {
607
608    $result = false;
609
610    if(!is_a($mailer, 'PHPMailer\PHPMailer\PHPMailer')) {
611        error_log("send_mail: PHPMailer object required!");
612        return $result;
613    }
614
615    if (!$mail) {
616        error_log("send_mail: no mail given, exiting...");
617        return $result;
618    }
619
620    /* Replace data in mail, subject and body */
621    foreach($data as $key => $value ) {
622        $mail = str_replace('{'.$key.'}', $value, $mail);
623        $mail_from = str_replace('{'.$key.'}', $value, $mail_from);
624        $subject = str_replace('{'.$key.'}', $value, $subject);
625        $body = str_replace('{'.$key.'}', $value, $body);
626    }
627
628    $mailer->setFrom($mail_from, $mail_from_name);
629    $mailer->addReplyTo($mail_from, $mail_from_name);
630    $mailer->addAddress($mail);
631    $mailer->Subject = $subject;
632    $mailer->Body = $body;
633
634    $result = $mailer->send();
635
636    if (!$result) {
637        error_log("send_mail: ".$mailer->ErrorInfo);
638    }
639
640    return $result;
641
642}
643
644/* @function string check_username_validity(string $username, string $login_forbidden_chars)
645 * Check the user name against a regex or internal ctype_alnum() call to make sure the username doesn't contain
646 * predetermined bad values, like an '*' can allow an attacker to 'test' to find valid usernames.
647 * @param username the user name to test against
648 * @param optional login_forbidden_chars invalid characters
649 * @return $result
650 */
651function check_username_validity($username,$login_forbidden_chars) {
652    $result = "";
653
654    if (!$login_forbidden_chars) {
655        if (!ctype_alnum($username)) {
656            $result = "badcredentials";
657            error_log("Non alphanumeric characters in username $username");
658        }
659    }
660    else {
661        preg_match_all("/[$login_forbidden_chars]/", $username, $forbidden_res);
662        if (count($forbidden_res[0])) {
663            $result = "badcredentials";
664            error_log("Illegal characters in username $username (list of forbidden characters: $login_forbidden_chars)");
665        }
666    }
667
668    return $result;
669}
670
671/* @function string hook_command(string $hook, string  $login, string $newpassword, null|string $oldpassword, null|boolean $hook_password_encodebase64)
672   Creates the command line to execute for the prehook/posthook process. Passwords will be base64 encoded if configured. Base64 encoding will prevent passwords with special
673   characters to be modified by the escapeshellarg() function.
674   @param $hook string script/command to execute for procesing hook data
675   @param $login string username to change/set password for
676   @param $newpassword string new passwword for given login
677   @param $oldpassword string old password for given login
678   @param hook_password_encodebase64 boolean set to true if passwords are to be converted to base64 encoded strings
679*/
680function hook_command($hook, $login, $newpassword, $oldpassword = null, $hook_password_encodebase64 = false) {
681
682    $command = '';
683    if ( isset($hook_password_encodebase64) && $hook_password_encodebase64 ) {
684	$command = escapeshellcmd($hook).' '.escapeshellarg($login).' '.base64_encode($newpassword);
685
686	if ( ! is_null($oldpassword) ) {
687	    $command .= ' '.base64_encode($oldpassword);
688	}
689
690    } else {
691	$command = escapeshellcmd($hook).' '.escapeshellarg($login).' '.escapeshellarg($newpassword);
692
693	if ( ! is_null($oldpassword) ) {
694	    $command .= ' '.escapeshellarg($oldpassword);
695	}
696    }
697    return $command;
698}
699
700/* function allowed_rate(string $login, string $ip_addr, array $rrl_config)
701 * Check if this login / this IP reached the limit fixed
702 * @return bool allowed
703 */
704function allowed_rate($login,$ip_addr,$rrl_config) {
705    $now = time();
706    $fblock=1;
707    if ($rrl_config["max_per_user"] > 0) {
708        $rrludb = $rrl_config["dbdir"] . "/ssp_rrl_users.json";
709        if (!file_exists($rrludb)) {
710           file_put_contents($rrludb,"{}");
711       }
712        $dbfh = fopen($rrludb . ".lock","w");
713        if (!$dbfh) { throw new Exception('nowrite to '.$rrludb); }
714        flock($dbfh,LOCK_EX,$fblock);
715        $users = (array) json_decode(file_get_contents($rrludb));
716        $atts = [$now];
717        if (array_key_exists($login,$users)) {
718            foreach ($users[$login] as $when) {
719                if ( $when > ($now - $rrl_config['per_time']) ) {
720                    array_push($atts,$when);
721                }
722            }
723        }
724        $users[$login] = $atts;
725        file_put_contents($rrludb,json_encode($users));
726        flock($dbfh,LOCK_UN);
727        if (count($atts) > $rrl_config["max_per_user"]) {
728            return false;
729        }
730    }
731    if ($rrl_config["max_per_ip"] > 0) {
732        $rrlidb = $rrl_config["dbdir"] . "/ssp_rrl_ips.json";
733        if (!file_exists($rrlidb)) {
734           file_put_contents($rrlidb,"{}");
735        }
736        $dbfh = fopen($rrlidb . ".lock","w");
737        if (!$dbfh) { throw new Exception('nowrite to '.$rrludb); }
738        flock($dbfh,LOCK_EX,$fblock);
739        $ips = (array) json_decode(file_get_contents($rrlidb));
740        $atts = [$now];
741        if (array_key_exists($ip_addr,$ips)) {
742            foreach ($ips[$ip_addr] as $when) {
743                if ( $when > ($now - $rrl_config['per_time']) ) {
744                    array_push($atts,$when);
745                }
746            }
747        }
748        $ips[$ip_addr] = $atts;
749       file_put_contents($rrlidb,json_encode($ips));
750        flock($dbfh,LOCK_UN);
751        if (count($atts) > $rrl_config["max_per_ip"]) {
752            return false;
753        }
754    }
755    return true;
756}
757
758
759