Skip to content

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
darkv committed Nov 14, 2018
0 parents commit c11a631
Show file tree
Hide file tree
Showing 5 changed files with 318 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.DS_Store

wordlist.json
9 changes: 9 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
The MIT License (MIT)

Copyright 2018 Johann Werner

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
185 changes: 185 additions & 0 deletions PasswordGenerator.class.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?php

/**
* A password generator that generates memorable passwords similar to the
* macOS keychain. For this it uses a public RSS feed to build up a list of
* words to be used in passwords.
*
* After successfully creating a list of words that list is written to disk
* in the file <i>wordlist.json</i>. The next time you create an instance
* of this class and the URL is unavailable that cached version is then used.
*
* Consecutive password generations with the very same instance won't
* recreate the word list but reuse the former one.
*
* @example PasswordGenerator.example.php Class in action.
*
* @author Johann Werner <johann.werner@posteo.de>
* @version 1.0.0
* @license MIT License
*/
class PasswordGenerator {
private $url;
private $minlength;
private $maxlength;
private $wordlist = [];
private $wordcache = 'wordlist.json';

/**
* Creates an instance of the password generator. You can pass an optional
* array with values that should override the default values for the keys:
* <dl>
* <dt>url</dt>
* <dd>The URL to fetch XML from which is used to create a wordlist from
* it's description nodes.</dd>
* <dt>minlength</dt>
* <dd>The miminum length of characters a word must have.</dd>
* <dt>maxlength</dt>
* <dd>The maxinum length of characters a word must have.</dd>
* </dl>
*
* @param array optional config array
* @param boolean true if data from URL should be fetched, false to use
* only cached wordlist; defaults to true
*
* @throws InvalidArgumentException if the URL is not valid
*/
public function __construct($params = [], $fetch = true) {
foreach($params as $key => $value) {
$this->$key = $value;
}
if ($fetch) {
if (!isset($this->url) || !filter_var($this->url, FILTER_VALIDATE_URL)) {
throw new InvalidArgumentException('Invalid URL: ' . $this->url);
}
if ($this->minlength > $this->maxlength) {
throw new InvalidArgumentException('Invalid word lengths: min='
. $this->minlength . ' max=' . $this->maxlength);
}
$this->populate_wordlist();
} else {
$this->read_wordlist();
}
}

/**
* Creates an instance of password generator that will use German wordlist.
*
* @static
* @return PasswordGenerator configured instance
*/
public static function DE() {
return new self([
'url' => 'http://www.tagesschau.de/newsticker.rdf',
'minlength' => 8,
'maxlength' => 15,
]);
}

/**
* Creates an instance of password generator that will use English wordlist.
*
* @static
* @return PasswordGenerator configured instance
*/
public static function EN() {
return new self([
'url' => 'http://rss.dw.com/rdf/rss-en-all',
'minlength' => 4,
'maxlength' => 12,
]);
}

/**
* Creates an instance of password generator that will use the cached wordlist.
* No HTTP reqeust to the URL source will be made. Be sure that you have an
* appropriate file <i>wordlist.json</i> present.
*
* @static
* @return PasswordGenerator configured instance that uses cache only
*/
public static function CACHED() {
return new self([], false);
}

/**
* Generates a password and returns it. If the used wordlist is empty and no
* password can be generated the value null is returned.
*
* @return string|null generated password or null if there is no wordlist
*/
public function generate() {
$listlength = count($this->wordlist);
if ($listlength < 1) {
$this->read_wordlist();
$listlength = count($this->wordlist);
if ($listlength < 1) {
return null;
}
}

$words = [];
$times = 2;
while ($times--) {
$r = $this->random_int(0, $listlength - 1);
$words[] = $this->wordlist[$r];
}

return $words[0] . $this->random_int(1, 999) . chr($this->random_int(33, 47)) . $words[1];
}

private function populate_wordlist() {
$input = $this->get_url_data($this->url);
$doc = new DOMDocument();
@$doc->loadXML($input);
$descriptions = $doc->getElementsByTagName('description');
$wordlist = array();
foreach($descriptions as $description) {
$text = $description->textContent;
$words = explode(' ', $text);
foreach($words as $word) {
$cleanword = preg_replace('/[,.;:?!\'"]+/', '', trim($word));
$wordlength = strlen($cleanword);

if ($wordlength >= $this->minlength && $wordlength <= $this->maxlength && ctype_alpha($cleanword)) {
$wordlist[strtoupper(substr($cleanword, 0, 1)) . strtolower(substr($cleanword, 1))] = 1;
}
}
}
$this->wordlist = array_keys($wordlist);

if (count($wordlist) > 0) {
$this->save_wordlist();
}
}

private function save_wordlist() {
file_put_contents($this->wordcache, json_encode($this->wordlist));
}

private function read_wordlist() {
if (file_exists($this->wordcache)) {
$this->wordlist = json_decode(file_get_contents($this->wordcache), true);
}
}

private function random_int($low, $high) {
if (version_compare(PHP_VERSION, '7.0.0', '<')) {
return rand($low, $high);
}
return random_int($low, $high);
}

private function get_url_data($url) {
$ch = curl_init();
curl_setopt($ch, CURLOPT_URL, $url);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, 5);
$data = curl_exec($ch);
curl_close($ch);

return $data;
}
}

?>
32 changes: 32 additions & 0 deletions PasswordGenerator.example.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?php

include 'PasswordGenerator.class.php';


// create instance with German word list
$gen = PasswordGenerator::DE();
// generate a password
echo $gen->generate(), "\n";

// reuse the existing wordlist without triggering a new HTTP request
echo $gen->generate(), "\n";


// create instance with English word list
$gen = PasswordGenerator::EN();
// generate a password
echo $gen->generate(), "\n";
echo $gen->generate(), "\n";
echo $gen->generate(), "\n";


// new instance with custom params
$gen = new PasswordGenerator([
'url' => 'http://www.tagesschau.de/newsticker.rdf',
'minlength' => 3,
'maxlength' => 6,
]);
// generate a password
echo $gen->generate(), "\n";

?>
89 changes: 89 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# php-password-generator

The PHP class PasswordGenerator serves as a password generator to create memorable passwords like the macOS keychain does.

## Getting Started

Copy the file *PasswordGenerator.class.php* into your project and include it in your own PHP file(s) with `include 'PasswordGenerator.class.php';`. Then create an instance either by using the predefined static methods for specific languages or customize it yourself by using the standard constructor.

```php
include 'PasswordGenerator.class.php';

// create instance with English word list
$gen = PasswordGenerator::EN();

// generate a password
echo $gen->generate();
```

### Prerequisites

This class works with PHP &gt;= 5.4 and needs a working internet connection.

### Password Syntax

The generated passwords follow a specific syntax:

```
<random word><number between 1 and 999><special character><random word>
```

Some examples of generated passwords:

* Theyre778+Breakthrough
* Reforms13)Translated
* When249*Awards

## Word Lists

The class uses RSS feeds to build a word list from which random words are used for password generation. The class has some predefined configuration for the languages English and German but can be customized too:

```php
include 'PasswordGenerator.class.php';

// create instance with English word list
$gen = PasswordGenerator::EN();

// create instance with German word list
$gen = PasswordGenerator::DE();

// create instance with custom parameters
$gen = new PasswordGenerator([
'url' => 'http://www.tagesschau.de/newsticker.rdf',
'minlength' => 3,
'maxlength' => 6,
]);
```

The params *minlength* and *maxlength* denote the allowed lengths of the words from the URL source to get into the word list. If a word list has been successfully built that list is saved into the file `wordlist.json`. The next time you create an instance of PasswordGenerator and the URL source cannot be contacted or does not contain any usable words that cached list is loaded instead. If you reuse the very same instance the word list is reused so no further HTTP requests are generated.

```php
include 'PasswordGenerator.class.php';

$gen = PasswordGenerator::EN();

// reuse word list without rebuilding
echo 'Password 1: ', $gen->generate();
echo 'Password 2: ', $gen->generate();
echo 'Password 3: ', $gen->generate();
```

### Caching

If you need to use that class in contexts where you do not have an internet connection you can prebuild a word list and copy the generated `wordlist.json` file into your project. When using the PasswordGenerator you can tell it to only use that cached list and skip the URL source request:

```php
include 'PasswordGenerator.class.php';

$gen = PasswordGenerator::CACHED();

echo $gen->generate();
```

### URL Sources

As source for word lists this class uses a configurable RSS feed. The feed has to be in XML format and contain *description* tags from which the textual content is extracted.

## License

This project is licensed under the MIT License - see the [LICENSE.md](LICENSE.md) file for details.

0 comments on commit c11a631

Please sign in to comment.