Skip to content

Commit 9aea263

Browse files
committed
Fix
1 parent 8b89b20 commit 9aea263

28 files changed

+1241
-3
lines changed

PasswordHelper.php

Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
1+
2+
<?php
3+
// PasswordHelper.php
4+
require 'vendor/autoload.php';
5+
6+
use ZxcvbnPhp\Zxcvbn;
7+
8+
class PasswordHelper {
9+
10+
private const DEFAULT_SYMBOLS = '!@#$%^&*()_+-=[]{}|;:,.<>/?~';
11+
public const DEFAULT_DICTIONARY = '/usr/share/dict/words';
12+
13+
public static function generatePassword(int $length, bool $useLowercase = true, bool $useUppercase = true, bool $useNumbers = true, bool $useSymbols = true): string {
14+
if ($length < 1) {
15+
throw new InvalidArgumentException("Password length must be at least 1.");
16+
}
17+
$charset = '';
18+
$passwordParts = [];
19+
$charTypesSelected = 0;
20+
if ($useLowercase) { $charset .= 'abcdefghijklmnopqrstuvwxyz'; $passwordParts[] = 'abcdefghijklmnopqrstuvwxyz'[random_int(0, 25)]; $charTypesSelected++; }
21+
if ($useUppercase) { $charset .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; $passwordParts[] = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'[random_int(0, 25)]; $charTypesSelected++; }
22+
if ($useNumbers) { $charset .= '0123456789'; $passwordParts[] = '0123456789'[random_int(0, 9)]; $charTypesSelected++; }
23+
if ($useSymbols) { $charset .= self::DEFAULT_SYMBOLS; $passwordParts[] = self::DEFAULT_SYMBOLS[random_int(0, strlen(self::DEFAULT_SYMBOLS) - 1)]; $charTypesSelected++; }
24+
if (empty($charset)) {
25+
throw new InvalidArgumentException("At least one character type must be selected for password generation.");
26+
}
27+
if ($length < $charTypesSelected) {
28+
$passwordParts = array_slice($passwordParts, 0, $length);
29+
$remainingLength = 0;
30+
} else {
31+
$remainingLength = $length - count($passwordParts);
32+
}
33+
for ($i = 0; $i < $remainingLength; $i++) {
34+
$passwordParts[] = $charset[random_int(0, strlen($charset) - 1)];
35+
}
36+
shuffle($passwordParts);
37+
return implode('', $passwordParts);
38+
}
39+
40+
public static function generatePassphrase(
41+
int $wordCount = 4,
42+
string $separator = '-',
43+
string $dictionaryFile = self::DEFAULT_DICTIONARY,
44+
int $minWordLength = 4,
45+
int $maxWordLength = 8,
46+
bool $capitalizeWords = true,
47+
bool $addNumber = true,
48+
bool $addSymbol = true
49+
): string {
50+
if ($wordCount < 1) {
51+
throw new InvalidArgumentException("Word count must be at least 1.");
52+
}
53+
if (!file_exists($dictionaryFile) || !is_readable($dictionaryFile)) {
54+
$altDictionaryFile = '/usr/dict/words';
55+
if (file_exists($altDictionaryFile) && is_readable($altDictionaryFile)) {
56+
$dictionaryFile = $altDictionaryFile;
57+
} else {
58+
throw new RuntimeException("Dictionary file not found or not readable: " . htmlspecialchars($dictionaryFile) . " (and alternative not found). Please ensure a dictionary is available.");
59+
}
60+
}
61+
62+
$words = file($dictionaryFile, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
63+
if ($words === false || empty($words)) {
64+
throw new RuntimeException("Dictionary is empty or could not be read: " . htmlspecialchars($dictionaryFile));
65+
}
66+
67+
$filteredWords = array_filter($words, function($word) use ($minWordLength, $maxWordLength) {
68+
$asciiWord = '';
69+
if (function_exists('iconv')) {
70+
$asciiWord = iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $word);
71+
if ($asciiWord === false) $asciiWord = $word;
72+
$asciiWord = strtolower($asciiWord);
73+
$asciiWord = preg_replace('/[^a-z]/', '', $asciiWord);
74+
} else {
75+
if (preg_match('/^[a-zA-Z]+$/', $word)) {
76+
$asciiWord = strtolower($word);
77+
} else {
78+
return false;
79+
}
80+
}
81+
if (empty($asciiWord)) return false;
82+
$len = strlen($asciiWord);
83+
return ($len >= $minWordLength && $len <= $maxWordLength);
84+
});
85+
86+
if (count($filteredWords) < $wordCount && count($filteredWords) > 0) {
87+
throw new RuntimeException("Not enough suitable words found in dictionary (need $wordCount, found " . count($filteredWords) . " matching criteria " . $minWordLength . "-" . $maxWordLength . " chars, a-z only). Try adjusting word length constraints or using a larger/different dictionary.");
88+
} elseif (empty($filteredWords)) {
89+
throw new RuntimeException("No suitable words found in the dictionary matching criteria (length " . $minWordLength . "-" . $maxWordLength . ", a-z only).");
90+
}
91+
92+
$filteredWords = array_values($filteredWords);
93+
$passphraseParts = [];
94+
$keys = [];
95+
96+
if (count($filteredWords) > 0) {
97+
$numWordsToSelect = min($wordCount, count($filteredWords));
98+
if ($numWordsToSelect < 1) throw new RuntimeException("Cannot select 0 words.");
99+
$keys = (array) array_rand($filteredWords, $numWordsToSelect);
100+
}
101+
102+
if (empty($keys)) {
103+
throw new RuntimeException("Could not select random words (keys array is empty).");
104+
}
105+
106+
foreach ($keys as $key) {
107+
$word = $filteredWords[$key];
108+
if ($capitalizeWords) {
109+
$word = ucfirst($word);
110+
}
111+
$passphraseParts[] = $word;
112+
}
113+
114+
$finalPassphrase = implode($separator, $passphraseParts);
115+
116+
if ($addNumber) {
117+
$finalPassphrase .= random_int(0, 9);
118+
}
119+
120+
if ($addSymbol && !empty(self::DEFAULT_SYMBOLS)) {
121+
$symbolIndex = random_int(0, strlen(self::DEFAULT_SYMBOLS) - 1);
122+
$finalPassphrase .= self::DEFAULT_SYMBOLS[$symbolIndex];
123+
}
124+
125+
return $finalPassphrase;
126+
}
127+
128+
public static function analyzePassword(string $password): array {
129+
$analysis = [];
130+
$zxcvbn = new Zxcvbn();
131+
$analysis['zxcvbn_full_results'] = $zxcvbn->passwordStrength($password);
132+
$analysis['zxcvbn'] = $analysis['zxcvbn_full_results'];
133+
$charLength = function_exists('mb_strlen') ? mb_strlen($password, 'UTF-8') : strlen($password);
134+
if (!function_exists('mb_strlen')) {
135+
$analysis['warning_strlen_fallback'] = "PHP mbstring extension not available; password length might be byte count for UTF-8 strings.";
136+
}
137+
$owasp = [
138+
'length' => $charLength,
139+
'has_lowercase' => (bool) preg_match('/[a-z]/u', $password),
140+
'has_uppercase' => (bool) preg_match('/[A-Z]/u', $password),
141+
'has_number' => (bool) preg_match('/[0-9]/', $password),
142+
'has_symbol' => (bool) preg_match('/[' . preg_quote(self::DEFAULT_SYMBOLS, '/') . ']/', $password),
143+
'is_strong' => false
144+
];
145+
$analysis['owasp_char_types_detected'] = [
146+
'lowercase_detected' => $owasp['has_lowercase'], 'uppercase_detected' => $owasp['has_uppercase'],
147+
'number_detected' => $owasp['has_number'], 'symbol_from_set_detected' => $owasp['has_symbol']
148+
];
149+
$owasp['rules_passed'] = 0;
150+
if ($owasp['length'] >= 10) $owasp['rules_passed']++;
151+
if ($owasp['has_lowercase']) $owasp['rules_passed']++;
152+
if ($owasp['has_uppercase']) $owasp['rules_passed']++;
153+
if ($owasp['has_number']) $owasp['rules_passed']++;
154+
if ($owasp['has_symbol']) $owasp['rules_passed']++;
155+
$diversityScore = ($owasp['has_lowercase'] ? 1:0) + ($owasp['has_uppercase'] ? 1:0) + ($owasp['has_number'] ? 1:0) + ($owasp['has_symbol'] ? 1:0);
156+
if ($owasp['length'] >= 10 && $diversityScore >= 3) {
157+
$owasp['is_strong'] = true;
158+
$owasp['compliance_message'] = "Passes basic OWASP-like policy.";
159+
} else {
160+
$owasp['is_strong'] = false;
161+
$owasp['compliance_message'] = "Fails basic OWASP-like policy (min length 10 and at least 3 character types).";
162+
}
163+
$analysis['owasp'] = $owasp;
164+
$baseAlphabetForEntropyString = '';
165+
if ($owasp['has_lowercase']) $baseAlphabetForEntropyString .= 'abcdefghijklmnopqrstuvwxyz';
166+
if ($owasp['has_uppercase']) $baseAlphabetForEntropyString .= 'ABCDEFGHIJKLMNOPQRSTUVWXYZ';
167+
if ($owasp['has_number']) $baseAlphabetForEntropyString .= '0123456789';
168+
if ($owasp['has_symbol']) $baseAlphabetForEntropyString .= self::DEFAULT_SYMBOLS;
169+
$strlenOfBaseAlphabet = function_exists('mb_strlen') ? mb_strlen($baseAlphabetForEntropyString, '8bit') : strlen($baseAlphabetForEntropyString);
170+
$baseAlphabetArray = str_split($baseAlphabetForEntropyString);
171+
$uniqueAlphabetChars = array_unique($baseAlphabetArray);
172+
$alphabetSize = count($uniqueAlphabetChars);
173+
$analysis['debug_alphabet_calculation_details'] = [
174+
'1_concatenated_base_string' => $baseAlphabetForEntropyString,
175+
'2_strlen_of_concatenated_string_(expected_byte_count)' => $strlenOfBaseAlphabet,
176+
'4_count_after_str_split' => count($baseAlphabetArray),
177+
'5_array_after_array_unique' => array_values($uniqueAlphabetChars),
178+
'6_final_alphabet_size_used' => $alphabetSize,
179+
'7_reconstructed_unique_alphabet_string' => implode('', $uniqueAlphabetChars)
180+
];
181+
if ($alphabetSize > 1 && $owasp['length'] > 0) {
182+
$analysis['shannon_theoretical_entropy'] = round($owasp['length'] * log($alphabetSize, 2), 2);
183+
$analysis['alphabet_size_for_theoretical_entropy'] = $alphabetSize;
184+
} else {
185+
$analysis['shannon_theoretical_entropy'] = 0;
186+
$analysis['alphabet_size_for_theoretical_entropy'] = $alphabetSize;
187+
}
188+
$nodeScriptPath = __DIR__ . '/analyze_tai.js';
189+
$command = '/usr/bin/node ' . escapeshellarg($nodeScriptPath) . ' ' . escapeshellarg($password) . ' 2>&1';
190+
$nodeOutputJson = shell_exec($command);
191+
$analysis['node_script_raw_output'] = $nodeOutputJson;
192+
if ($nodeOutputJson === null || $nodeOutputJson === '') {
193+
$analysis['node_script_execution_error'] = 'Failed to execute Node.js script or script returned empty output.';
194+
$analysis['tai'] = ['error' => 'Node script execution error'];
195+
$analysis['owasp_npm_results'] = ['error' => 'Node script execution error'];
196+
$analysis['fast_entropy_results'] = ['error' => 'Node script execution error'];
197+
$analysis['string_entropy_results'] = ['error' => 'Node script execution error'];
198+
} else {
199+
$allNodeResults = json_decode($nodeOutputJson, true);
200+
if (json_last_error() === JSON_ERROR_NONE && is_array($allNodeResults)) {
201+
$analysis['tai'] = $allNodeResults['tai'] ?? ['error' => 'TAI data missing from Node script output'];
202+
$analysis['owasp_npm_results'] = $allNodeResults['owasp_npm'] ?? ['error' => 'OWASP NPM data missing from Node script output'];
203+
$analysis['fast_entropy_results'] = $allNodeResults['fast_entropy'] ?? ['error' => 'Fast Entropy data missing from Node script output'];
204+
$analysis['string_entropy_results'] = $allNodeResults['string_entropy'] ?? ['error' => 'String Entropy data missing from Node script output'];
205+
} else {
206+
$analysis['node_script_parsing_error'] = 'Failed to parse JSON from Node.js script output. The output might contain raw error messages.';
207+
$analysis['node_script_json_error_message'] = json_last_error_msg();
208+
$analysis['tai'] = ['error' => 'Node script JSON parsing error, or script failed. Check raw output.'];
209+
$analysis['owasp_npm_results'] = ['error' => 'Node script JSON parsing error, or script failed.'];
210+
$analysis['fast_entropy_results'] = ['error' => 'Node script JSON parsing error, or script failed.'];
211+
$analysis['string_entropy_results'] = ['error' => 'Node script JSON parsing error, or script failed.'];
212+
}
213+
}
214+
return $analysis;
215+
}
216+
}
217+
?>

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -78,8 +78,9 @@ Ensure your server (e.g., Debian/Ubuntu based) has the following installed:
7878
```bash
7979
composer install
8080
```
81-
This will create a `vendor/` directory.
82-
Alternatively, download the files (index.php, PasswordHelper.php, analyze_tai.js, composer.json, package.json) and place them in the directory.
81+
82+
This will create a `vendor/` directory.
83+
Alternatively, download the files (index.php, PasswordHelper.php, analyze_tai.js, composer.json, package.json) and place them in the directory.
8384

8485
3. **Install Node.js Dependencies (npm):**
8586
The project uses several Node.js packages for analysis. Install them by running:
@@ -244,4 +245,3 @@ This tool uses multiple analyzers to provide a holistic view of password/passphr
244245
<p align="center">
245246
<sub><sup>With ❤️ by <a href="https://github.com/deuza">DeuZa</a></sup></sub>
246247
</p>
247-

analyze_tai.js

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
// analyze_tai.js
2+
const taiPasswordStrength = require('tai-password-strength');
3+
const owaspPasswordStrengthTest = require('owasp-password-strength-test');
4+
const fastPasswordEntropyFunction = require('fast-password-entropy');
5+
const stringEntropyModule = require('string-entropy'); // Import the module object
6+
7+
const password = process.argv[2];
8+
9+
if (!password) {
10+
console.error(JSON.stringify({ error: "No password provided to TAI script" }));
11+
process.exit(1);
12+
}
13+
14+
const results = {};
15+
16+
// --- TAI Analysis ---
17+
try {
18+
const PasswordStrengthClass = taiPasswordStrength.PasswordStrength;
19+
if (!PasswordStrengthClass) {
20+
results.tai = { error: "TAI Internal Error: PasswordStrength class could not be loaded." };
21+
} else {
22+
const strength = new PasswordStrengthClass();
23+
strength.addCommonPasswords(taiPasswordStrength.commonPasswords);
24+
const taiResults = strength.check(password);
25+
26+
const TAI_STRENGTH_MEANINGS = {
27+
VERY_WEAK: 'Very Weak', WEAK: 'Weak', REASONABLE: 'Reasonable',
28+
MEDIUM: 'Medium', STRONG: 'Strong', VERY_STRONG: 'Very Strong'
29+
};
30+
taiResults.strengthMeaning = TAI_STRENGTH_MEANINGS[taiResults.strengthCode] || taiResults.strengthCode || 'N/A';
31+
results.tai = taiResults;
32+
}
33+
} catch (e) {
34+
results.tai = {
35+
error: "Error during TAI password analysis",
36+
details: e.message,
37+
stack: e.stack
38+
};
39+
}
40+
41+
// --- OWASP Password Strength Test (npm) Analysis ---
42+
try {
43+
const owaspResults = owaspPasswordStrengthTest.test(password);
44+
results.owasp_npm = owaspResults;
45+
} catch (e) {
46+
results.owasp_npm = {
47+
error: "Error during OWASP (npm) analysis",
48+
details: e.message,
49+
stack: e.stack
50+
};
51+
}
52+
53+
// --- Fast Password Entropy Analysis ---
54+
try {
55+
results.fast_entropy = {
56+
shannonEntropyBits: fastPasswordEntropyFunction(password)
57+
};
58+
} catch (e) {
59+
results.fast_entropy = {
60+
error: "Error during Fast Password Entropy analysis",
61+
details: e.message,
62+
stack: e.stack
63+
};
64+
}
65+
66+
// --- String Entropy Analysis ---
67+
try {
68+
// Corrected call: using the 'entropy' method from the imported module object
69+
if (stringEntropyModule && typeof stringEntropyModule.entropy === 'function') {
70+
results.string_entropy = {
71+
shannonEntropyBits: stringEntropyModule.entropy(password)
72+
};
73+
} else {
74+
results.string_entropy = {
75+
error: "String Entropy module structure unexpected.",
76+
details: "Expected an object with an 'entropy' function based on previous tests."
77+
};
78+
// console.error("Debug (analyze_tai.js): stringEntropyModule content was: " + JSON.stringify(stringEntropyModule));
79+
}
80+
} catch (e) {
81+
results.string_entropy = {
82+
error: "Error during String Entropy analysis",
83+
details: e.message,
84+
stack: e.stack
85+
};
86+
}
87+
88+
console.log(JSON.stringify(results));

composer.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"require": {
3+
"bjeavons/zxcvbn-php": "^1.3"
4+
},
5+
"config": {
6+
"optimize-autoloader": true,
7+
"preferred-install": "dist"
8+
},
9+
"minimum-stability": "stable",
10+
"prefer-stable": true
11+
}

favicon.ico

9.35 KB
Binary file not shown.

images/01.png

54 KB
Loading

images/128_0.png

32.1 KB
Loading

images/128_1.png

36.6 KB
Loading

images/128_2.png

36.2 KB
Loading

images/128_3.png

24.7 KB
Loading

0 commit comments

Comments
 (0)