initialization
This commit is contained in:
commit
9ff09693ed
92 files changed
+46209
No files matched your search
@@ -0,0 +1,317 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* PHP fallback implementation for pwgen logic (pronounceable passwords).
|
||||
*
|
||||
* Based on NIST FIPS 181 "Automated Password Generator" standard
|
||||
* and inspired by roderik/pwgen-php (https://github.com/roderik/pwgen-php)
|
||||
*
|
||||
* Original pwgen concept:
|
||||
* Copyright (c) 2001 Theodore Ts'o <tytso@alum.mit.edu>
|
||||
*
|
||||
* PHP implementation credits:
|
||||
* Copyright (c) 2012 Roderik van der Veer
|
||||
* BSD License (see https://github.com/roderik/pwgen-php/blob/master/LICENSE)
|
||||
* Original source: https://github.com/roderik/pwgen-php
|
||||
*
|
||||
* Modernized for PHP 8+ by Password Generator Project, 2025.
|
||||
* This class provides a fallback for pwgen-style password generation when
|
||||
* the native pwgen executable is not available.
|
||||
*
|
||||
* Based on NIST FIPS 181 - Federal Information Processing Standards Publication 181
|
||||
* "Automated Password Generator" (1993)
|
||||
*/
|
||||
class PwGenPhpNg
|
||||
{
|
||||
// Character classification flags (based on NIST FIPS 181)
|
||||
private const VOWEL = 0x01;
|
||||
private const CONSONANT = 0x02;
|
||||
private const DIPHTHONG = 0x04;
|
||||
private const NOT_FIRST = 0x08;
|
||||
|
||||
// Unit definitions with pronunciability rules
|
||||
private array $elements = [
|
||||
'a' => ['flags' => self::VOWEL, 'weight' => 4],
|
||||
'ae' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||
'ah' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||
'ai' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||
'b' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'c' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'ch' => ['flags' => self::CONSONANT | self::DIPHTHONG, 'weight' => 1],
|
||||
'd' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'e' => ['flags' => self::VOWEL, 'weight' => 4],
|
||||
'ee' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||
'ei' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||
'f' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'g' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'gh' => ['flags' => self::CONSONANT | self::DIPHTHONG | self::NOT_FIRST, 'weight' => 1],
|
||||
'h' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'i' => ['flags' => self::VOWEL, 'weight' => 4],
|
||||
'ie' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||
'j' => ['flags' => self::CONSONANT, 'weight' => 1],
|
||||
'k' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'l' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'm' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'n' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'ng' => ['flags' => self::CONSONANT | self::DIPHTHONG | self::NOT_FIRST, 'weight' => 1],
|
||||
'o' => ['flags' => self::VOWEL, 'weight' => 4],
|
||||
'oh' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||
'oo' => ['flags' => self::VOWEL | self::DIPHTHONG, 'weight' => 1],
|
||||
'p' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'ph' => ['flags' => self::CONSONANT | self::DIPHTHONG, 'weight' => 1],
|
||||
'qu' => ['flags' => self::CONSONANT | self::DIPHTHONG, 'weight' => 1],
|
||||
'r' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'rh' => ['flags' => self::CONSONANT | self::DIPHTHONG, 'weight' => 1],
|
||||
's' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'sh' => ['flags' => self::CONSONANT | self::DIPHTHONG, 'weight' => 1],
|
||||
't' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'th' => ['flags' => self::CONSONANT | self::DIPHTHONG, 'weight' => 1],
|
||||
'u' => ['flags' => self::VOWEL, 'weight' => 4],
|
||||
'v' => ['flags' => self::CONSONANT, 'weight' => 1],
|
||||
'w' => ['flags' => self::CONSONANT, 'weight' => 2],
|
||||
'wh' => ['flags' => self::CONSONANT | self::DIPHTHONG, 'weight' => 1],
|
||||
'x' => ['flags' => self::CONSONANT | self::NOT_FIRST, 'weight' => 1],
|
||||
'y' => ['flags' => self::VOWEL, 'weight' => 2],
|
||||
'z' => ['flags' => self::CONSONANT, 'weight' => 1],
|
||||
];
|
||||
|
||||
private array $vowels = [];
|
||||
private array $consonants = [];
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->initializePhonemes();
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize phoneme arrays based on element classification
|
||||
*/
|
||||
private function initializePhonemes(): void
|
||||
{
|
||||
foreach ($this->elements as $phoneme => $properties) {
|
||||
if ($properties['flags'] & self::VOWEL) {
|
||||
for ($i = 0; $i < $properties['weight']; $i++) {
|
||||
$this->vowels[] = $phoneme;
|
||||
}
|
||||
}
|
||||
if ($properties['flags'] & self::CONSONANT) {
|
||||
for ($i = 0; $i < $properties['weight']; $i++) {
|
||||
$this->consonants[] = $phoneme;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate pronounceable passwords using pwgen logic
|
||||
*
|
||||
* @param int $length Minimum length of password
|
||||
* @param int $count Number of passwords to generate
|
||||
* @param bool $includeNumbers Include numbers in passwords
|
||||
* @param bool $includeCapitals Include capital letters
|
||||
* @return array Array of generated passwords
|
||||
*/
|
||||
public function generate(int $length = 8, int $count = 1, bool $includeNumbers = true, bool $includeCapitals = true): array
|
||||
{
|
||||
if ($length < 4) {
|
||||
$length = 4; // Minimum for pronounceable passwords
|
||||
}
|
||||
|
||||
$passwords = [];
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$password = $this->generatePassword($length, $includeNumbers, $includeCapitals);
|
||||
$passwords[] = $password;
|
||||
}
|
||||
|
||||
return $passwords;
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a single pronounceable password
|
||||
*/
|
||||
private function generatePassword(int $length, bool $includeNumbers, bool $includeCapitals): string
|
||||
{
|
||||
$password = '';
|
||||
$currentLength = 0;
|
||||
$lastWasVowel = false;
|
||||
$syllableLength = 0;
|
||||
$maxAttempts = 100;
|
||||
$attempts = 0;
|
||||
|
||||
while ($currentLength < $length && $attempts < $maxAttempts) {
|
||||
$attempts++;
|
||||
|
||||
// Start new syllable with consonant, then alternate
|
||||
if ($syllableLength === 0 || ($syllableLength > 0 && $lastWasVowel)) {
|
||||
// Choose consonant
|
||||
$phoneme = $this->selectConsonant($currentLength === 0);
|
||||
$lastWasVowel = false;
|
||||
} else {
|
||||
// Choose vowel
|
||||
$phoneme = $this->selectVowel();
|
||||
$lastWasVowel = true;
|
||||
}
|
||||
|
||||
// Check if adding this phoneme exceeds length
|
||||
if ($currentLength + strlen($phoneme) > $length) {
|
||||
// Try to find a shorter phoneme or end the password
|
||||
if ($lastWasVowel) {
|
||||
$phoneme = $this->selectVowel(1); // Single character vowel
|
||||
} else {
|
||||
$phoneme = $this->selectConsonant(false, 1); // Single character consonant
|
||||
}
|
||||
|
||||
if ($currentLength + strlen($phoneme) > $length) {
|
||||
break; // Can't fit anything more
|
||||
}
|
||||
}
|
||||
|
||||
$password .= $phoneme;
|
||||
$currentLength += strlen($phoneme);
|
||||
$syllableLength++;
|
||||
|
||||
// End syllable after 2-4 phonemes
|
||||
if ($syllableLength >= 2 && random_int(0, 2) === 0) {
|
||||
$syllableLength = 0;
|
||||
$lastWasVowel = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Apply post-processing
|
||||
if ($includeCapitals) {
|
||||
$password = $this->addCapitalization($password);
|
||||
}
|
||||
|
||||
if ($includeNumbers && strlen($password) < $length) {
|
||||
$password = $this->addNumbers($password, $length);
|
||||
}
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a consonant phoneme
|
||||
*/
|
||||
private function selectConsonant(bool $isFirst = false, int $maxLength = PHP_INT_MAX): string
|
||||
{
|
||||
$candidates = [];
|
||||
|
||||
foreach ($this->consonants as $consonant) {
|
||||
$element = $this->elements[$consonant];
|
||||
|
||||
// Skip if not allowed at beginning
|
||||
if ($isFirst && ($element['flags'] & self::NOT_FIRST)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Skip if too long
|
||||
if (strlen($consonant) > $maxLength) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$candidates[] = $consonant;
|
||||
}
|
||||
|
||||
if (empty($candidates)) {
|
||||
return 'b'; // Fallback
|
||||
}
|
||||
|
||||
return $candidates[random_int(0, count($candidates) - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Select a vowel phoneme
|
||||
*/
|
||||
private function selectVowel(int $maxLength = PHP_INT_MAX): string
|
||||
{
|
||||
$candidates = [];
|
||||
|
||||
foreach ($this->vowels as $vowel) {
|
||||
if (strlen($vowel) <= $maxLength) {
|
||||
$candidates[] = $vowel;
|
||||
}
|
||||
}
|
||||
|
||||
if (empty($candidates)) {
|
||||
return 'a'; // Fallback
|
||||
}
|
||||
|
||||
return $candidates[random_int(0, count($candidates) - 1)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Add capitalization to password (typically first letter)
|
||||
*/
|
||||
private function addCapitalization(string $password): string
|
||||
{
|
||||
if (strlen($password) === 0) {
|
||||
return $password;
|
||||
}
|
||||
|
||||
// Capitalize first letter
|
||||
return ucfirst(strtolower($password));
|
||||
}
|
||||
|
||||
/**
|
||||
* Add numbers to password if there's remaining length
|
||||
*/
|
||||
private function addNumbers(string $password, int $targetLength): string
|
||||
{
|
||||
$remaining = $targetLength - strlen($password);
|
||||
|
||||
if ($remaining <= 0) {
|
||||
return $password;
|
||||
}
|
||||
|
||||
// Add 1-2 numbers
|
||||
$numbersToAdd = min($remaining, random_int(1, 2));
|
||||
|
||||
for ($i = 0; $i < $numbersToAdd; $i++) {
|
||||
$password .= (string)random_int(0, 9);
|
||||
}
|
||||
|
||||
return $password;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a password meets basic pronounceability rules
|
||||
*/
|
||||
public function isPronounceablePassword(string $password): bool
|
||||
{
|
||||
if (strlen($password) < 3) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$vowelCount = 0;
|
||||
$consonantCount = 0;
|
||||
|
||||
// Simple heuristic: check for reasonable vowel/consonant distribution
|
||||
for ($i = 0; $i < strlen($password); $i++) {
|
||||
$char = strtolower($password[$i]);
|
||||
if (in_array($char, ['a', 'e', 'i', 'o', 'u', 'y'])) {
|
||||
$vowelCount++;
|
||||
} elseif (ctype_alpha($char)) {
|
||||
$consonantCount++;
|
||||
}
|
||||
}
|
||||
|
||||
// Must have at least one vowel and one consonant
|
||||
// And vowels shouldn't be more than 60% of letters
|
||||
$totalLetters = $vowelCount + $consonantCount;
|
||||
return $vowelCount >= 1 && $consonantCount >= 1 &&
|
||||
($totalLetters === 0 || $vowelCount / $totalLetters <= 0.6);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available phoneme elements (for debugging/inspection)
|
||||
*/
|
||||
public function getPhonemeCount(): array
|
||||
{
|
||||
return [
|
||||
'vowels' => count(array_unique($this->vowels)),
|
||||
'consonants' => count(array_unique($this->consonants)),
|
||||
'total_elements' => count($this->elements)
|
||||
];
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user