From 768720a83a8448fad559f10589354a19d041aaf0 Mon Sep 17 00:00:00 2001 From: Marcus Olsson Date: Fri, 16 Oct 2015 12:15:46 +0200 Subject: [PATCH] Initial commit. --- .editorconfig | 15 ++++ .gitignore | 2 + .travis.yml | 8 ++ LICENSE.md | 21 +++++ README.md | 152 +++++++++++++++++++++++++++++++++ composer.json | 50 +++++++++++ phpunit.xml | 18 ++++ src/Facades/Zxcvbn.php | 18 ++++ src/ZxcvbnServiceProvider.php | 81 ++++++++++++++++++ tests/ZxcvbnTest.php | 154 ++++++++++++++++++++++++++++++++++ 10 files changed, 519 insertions(+) create mode 100644 .editorconfig create mode 100644 .gitignore create mode 100644 .travis.yml create mode 100644 LICENSE.md create mode 100644 README.md create mode 100644 composer.json create mode 100644 phpunit.xml create mode 100644 src/Facades/Zxcvbn.php create mode 100644 src/ZxcvbnServiceProvider.php create mode 100644 tests/ZxcvbnTest.php diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..cd8eb86 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,15 @@ +; This file is for unifying the coding style for different editors and IDEs. +; More information at http://editorconfig.org + +root = true + +[*] +charset = utf-8 +indent_size = 4 +indent_style = space +end_of_line = lf +insert_final_newline = true +trim_trailing_whitespace = true + +[*.md] +trim_trailing_whitespace = false diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8b7ef35 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +/vendor +composer.lock diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 0000000..f30edda --- /dev/null +++ b/.travis.yml @@ -0,0 +1,8 @@ +language: php +php: + - 5.5 + - 5.6 + - 7.0 + - nightly + +before_script: composer install diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..9134fe3 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,21 @@ +# The MIT License (MIT) + +Copyright (c) 2015 Marcus Olsson + +> 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..41e30ed --- /dev/null +++ b/README.md @@ -0,0 +1,152 @@ +# Zxcvbn for Laravel 5 + +[![Latest Version on Packagist][ico-version]][link-packagist] +[![Software License][ico-license]](LICENSE.md) +[![Build Status][ico-travis]][link-travis] + +A simple implementation of zxcvbn for Laravel 5. This package allows you to access "zxcvbn-related" data on a passphrase in the application, but also use zxcvbn as a standard validator. + +Uses [Zxcvbn-PHP](https://github.com/bjeavons/zxcvbn-php) by [@bjeavons](https://github.com/bjeavons), which in turn is inspired by [zxcvbn](https://github.com/dropbox/zxcvbn) by [@dropbox](https://github.com/dropbox). + +## Install + +Via Composer + +``` bash +$ composer require olssonm/l5-zxcvbn +``` + +Add the package to your providers array: + +``` php +'providers' => [ + Olssonm\Zxcvbn\ZxcvbnServiceProvider::class, +] +``` + +If you wish to have the ability to use `Zxcvbn` via dependency injection, or just have a quick way to access the class – add an alias to the facades: + +``` php +'aliases' => [ + 'Zxcvbn' => Olssonm\Zxcvbn\Facades\Zxcvbn::class +] +``` + +## Usage + +If you've added `Olssonm\Zxcvbn` as an alias, your can access Zxcvbn easily from anywhere in your application: + +### "In app" + +``` php +passwordStrength('password')); + + // array:6 [ + // "crack_time" => 5.0E-5 + // "calc_time" => 0.1857271194458 + // "password" => "password" + // "entropy" => 0.0 + // "match_sequence" => array:1 [] + // "score" => 0 + // ] + } +} +?> +``` + +Play around with different passwords and phrases, the results may surprise you. Check out [Zxcvbn-PHP](https://github.com/bjeavons/zxcvbn-php) for more uses and examples. + +### As a validator + +The package gives you two different validation rules that you may use; `zxcvbn_min` and `zxcvbn_dictionary`. + +#### zxcvbn_min + +`zxcvbn_min` allows you to set up a rule for minimum score that the value beeing tested should adhere to. + +***Syntax*** +`'input' => 'zxcvbn_min:min_value'` + +``` php + 'password']; + $validator = Validator::make($data, [ + 'password' => 'zxcvbn_min:3|required', + ], [ + 'password.zxcvbn_min' => 'Your password is not strong enough!' + ]); +``` + +In this example the password should at least have a "score" of three (3) to pass the validation. Of course, you should probably use the zxcvbn-library on the front-end too to allow the user to know this before posting the form... + +#### zxcvbn_dictionary + +This is a bit more interesting. `zxcvbn_dictionary` allows you to input both the users username and/or email, and their password. The validator checks that the password doesn't exist in the username, or that they are too similar. + +***Syntax*** +`'input' => 'xcvbn_dictionary:username,email'` + +``` php + 'user', + 'email' => 'trash@thedumpster.com' + ]; + $validator = Validator::make($password, [ + 'password' => 'zxcvbn_dictionary:' . $data['username'] . ',' . $data['email'] . '|required', + ]); + + dd($validator->passes()); + // true + + /** + * Example 2, fail + */ + $password = 'mycomplicatedphrase'; + $data = [ + 'username' => 'mycomplicatedphrase', + 'email' => 'mycomplicatedphrase@thedumpster.com' + ]; + $validator = Validator::make($password, [ + 'password' => 'zxcvbn_dictionary:' . $data['username'] . ',' . $data['email'] . '|required', + ]); + + dd($validator->passes()); + // false +``` + +## Testing + +``` bash +$ composer test +``` + +or + +``` bash +$ phpunit +``` + +## License + +The MIT License (MIT). Please see [License File](LICENSE.md) for more information. + +© 2015 [Marcus Olsson](https://marcusolsson.me). + +[ico-version]: https://img.shields.io/packagist/v/olssonm/l5-zxcvbn.svg?style=flat-square +[ico-license]: https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square +[ico-travis]: https://img.shields.io/travis/olssonm/l5-zxcvbn/master.svg?style=flat-square +[link-packagist]: https://packagist.org/packages/olssonm/l5-zxcvbn +[link-travis]: https://travis-ci.org/olssonm/l5-zxcvbn diff --git a/composer.json b/composer.json new file mode 100644 index 0000000..b4ddfef --- /dev/null +++ b/composer.json @@ -0,0 +1,50 @@ +{ + "name": "olssonm/l5-zxcvbn", + "description": "Implementation of the zxcvbn project by @dropbox for Laravel 5. Uses zxcvbn-php by @bjeavons.", + "keywords": [ + "olssonm", + "zxcvbn", + "staple horse battery", + "passwords", + "validation", + "laravel" + ], + "homepage": "https://github.com/olssonm/l5-zxcvbn", + "license": "MIT", + "authors": [ + { + "name": "Marcus Olsson", + "email": "hello@marcusolsson.me", + "homepage": "https://marcusolsson.me" + } + ], + "require": { + "php" : ">=5.3.0", + "illuminate/support": "~5.1", + "bjeavons/zxcvbn-php": "0.1.4" + }, + "require-dev": { + "phpunit/phpunit" : "4.*", + "orchestra/testbench": "~3.0", + "scrutinizer/ocular": "~1.1" + }, + "autoload": { + "psr-4": { + "Olssonm\\Zxcvbn\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Olssonm\\Zxcvbn\\Test\\": "tests/" + } + }, + "scripts": { + "test": "phpunit" + }, + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "minimum-stability": "stable" +} diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..3347b75 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,18 @@ + + + + + ./tests/ + + + diff --git a/src/Facades/Zxcvbn.php b/src/Facades/Zxcvbn.php new file mode 100644 index 0000000..83b5543 --- /dev/null +++ b/src/Facades/Zxcvbn.php @@ -0,0 +1,18 @@ +passwordStrength($value); + $target = 5; + + if(isset($parameters[0])) { + $target = $parameters[0]; + } + + return ($zxcvbn['score'] >= $target); + }, 'Your :input is not secure enough.'); + + Validator::replacer('zxcvbn_min', function($message, $attribute, $rule, $parameters) { + $message = str_replace(':input', $attribute, $message); + return $message; + }); + + /** + * Extend the Laravel Validator with the "zxcvbn_min" rule + */ + Validator::extend('zxcvbn_dictionary', function($attribute, $value, $parameters, $validator) { + $email = null; + $username = null; + + if(isset($parameters[0])) { + $email = $parameters[0]; + $username = $parameters[1]; + } + + $zxcvbn = new ZxcvbnPhp(); + $zxcvbn = $zxcvbn->passwordStrength($value, [$username, $email]); + + if(isset($zxcvbn['match_sequence'][0])) { + $dictionary = $zxcvbn['match_sequence'][0]; + if(isset($dictionary->dictionaryName)) { + return false; + } + } + + return true; + + }, 'Your :input is insecure. It either matches a commonly used password, or you have used a similar username/password combination.'); + + Validator::replacer('zxcvbn_dictionary', function($message, $attribute, $rule, $parameters) { + $message = str_replace(':input', $attribute, $message); + return $message; + }); + } + + /** + * Register any package services. + * + * @return void + */ + public function register() + { + $this->app['zxcvbn'] = $this->app->share(function ($app) { + return new ZxcvbnPhp($this->app['config']); + }); + } +} diff --git a/tests/ZxcvbnTest.php b/tests/ZxcvbnTest.php new file mode 100644 index 0000000..f539c8f --- /dev/null +++ b/tests/ZxcvbnTest.php @@ -0,0 +1,154 @@ + 'Olssonm\Zxcvbn\Facades\Zxcvbn' + ]; + } + + /** + * Just run som standard tests to see that Zxcvbn is up to snuff and working + * @test + */ + public function test_zxcvbn_basics() + { + $testVar1 = Zxcvbn::passwordStrength('test'); + + // Check keys + $this->assertArrayHasKey('score', $testVar1); + $this->assertArrayHasKey('match_sequence', $testVar1); + $this->assertArrayHasKey('entropy', $testVar1); + $this->assertArrayHasKey('password', $testVar1); + $this->assertArrayHasKey('calc_time', $testVar1); + $this->assertArrayHasKey('crack_time', $testVar1); + + // Check score-value + $this->assertEquals(0, $testVar1['score']); + + // Run some more tests + $testVar2 = Zxcvbn::passwordStrength('dadaurka'); + $testVar3 = Zxcvbn::passwordStrength('staple horse battery'); + $testVar4 = Zxcvbn::passwordStrength('7E6k9axB*gwGHa&aZTohmD9Wr&NVs[b4'); //<-- 32 + + // Check score-value + $this->assertEquals(1, $testVar2['score']); + $this->assertEquals(4, $testVar3['score']); + $this->assertEquals(4, $testVar4['score']); + } + + /** @test */ + public function test_password_strength() + { + // Standard tests + $this->assertEquals(true, $this->validate_without_message_min('test', 0)); + $this->assertEquals(false, $this->validate_without_message_min('test', 4)); + + $this->assertEquals(true, $this->validate_without_message_min('staple horse battery', 3)); + $this->assertEquals(true, $this->validate_without_message_min('staple horse battery', 4)); + $this->assertEquals(false, $this->validate_without_message_min('staple horse battery', 5)); + } + + /** @test */ + public function test_password_strength_with_message() + { + // Standard message + $this->assertEquals('Your password is not secure enough.', $this->validate_with_message_min('staple horse battery', 5, null)); + $this->assertEquals('Just a message', $this->validate_with_message_min('test', 4, 'Just a message')); + } + + /** @test */ + public function test_password_dictionary() + { + // Standard tests + $this->assertEquals(false, $this->validate_without_message_dictionary('password', 'test@test.com', 'test')); + $this->assertEquals(false, $this->validate_without_message_dictionary('test', 'test@test.com', 'test')); + $this->assertEquals(false, $this->validate_without_message_dictionary('721ahsa!', '721ahsa@test.com', '721ahsa')); + + $this->assertEquals(true, $this->validate_without_message_dictionary('721ahsa!', 'dadaurka@test.com', 'dadaurka')); + $this->assertEquals(true, $this->validate_without_message_dictionary('asd912j!', 'myemail@test.com', 'username')); + $this->assertEquals(true, $this->validate_without_message_dictionary('asd912j!', 'trash@thedumpster.com', 'username')); + + $this->assertEquals(true, $this->validate_without_message_dictionary('asd912j!', null, 'username')); + $this->assertEquals(true, $this->validate_without_message_dictionary('asd912j!', null, null)); + } + + /** @test */ + public function test_password_dictionary_with_message() + { + // Standard message + $this->assertEquals('Your password is insecure. It either matches a commonly used password, or you have used a similar username/password combination.', $this->validate_with_message_dictionary('password', 'test@test.com', 'test', null)); + $this->assertEquals('Just a message', $this->validate_with_message_dictionary('test', 'test@test.com', 'test', 'Just a message')); + } + + private function validate_without_message_min($password, $min) + { + $data = ['password' => $password]; + $validator = Validator::make($data, [ + 'password' => 'zxcvbn_min:' . $min . '|required', + ]); + + return $validator->passes(); + } + + private function validate_with_message_min($password, $min, $message) + { + $data = ['password' => $password]; + $validator = Validator::make($data, [ + 'password' => 'zxcvbn_min:' . $min . '|required', + ], [ + 'password.zxcvbn_min' => $message + ]); + + $errors = $validator->errors(); + return $errors->first('password'); + } + + private function validate_without_message_dictionary($password, $email, $username) + { + $data = ['password' => $password]; + $validator = Validator::make($data, [ + 'password' => 'zxcvbn_dictionary:' . $username . ',' . $email . '|required', + ]); + + return $validator->passes(); + } + + private function validate_with_message_dictionary($password, $email, $username, $message) + { + $data = ['password' => $password]; + $validator = Validator::make($data, [ + 'password' => 'zxcvbn_dictionary:' . $username . ',' . $email . '|required', + ], [ + 'password.zxcvbn_dictionary' => $message + ]); + + $errors = $validator->errors(); + return $errors->first('password'); + } +}