From ffb28c62beff7492a90842a294bd680f165f324e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Thu, 14 Jun 2018 19:34:04 +0200 Subject: [PATCH 01/30] dev version --- CHANGELOG.md | 3 +++ appinfo/info.xml | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 586b2b4..3564381 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,8 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). +## [Unreleased] + ## [v4.0.0-rc2] ### Added - User active column @@ -65,6 +67,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Changed - Supported version of ownCloud, Nextcloud: ownCloud 10, Nextcloud 12 +[Unreleased]: https://github.com/nextcloud/user_sql/compare/v4.0.0-rc2...develop [v4.0.0-rc2]: https://github.com/nextcloud/user_sql/compare/v4.0.0-rc1...v4.0.0-rc2 [4.0.0-rc1]: https://github.com/nextcloud/user_sql/compare/v3.1.0...v4.0.0-rc1 [3.1.0]: https://github.com/nextcloud/user_sql/compare/v2.4.0...v3.1.0 diff --git a/appinfo/info.xml b/appinfo/info.xml index 8827794..0e5c94b 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -8,7 +8,7 @@ Retrieve the users and groups info. Allow the users to change their passwords. Sync the users' email addresses with the addresses stored by Nextcloud. - 4.0.0-rc2 + 4.0.0-dev agpl Andreas Böhler <dev (at) aboehler (dot) at> Marcin Łojewski <dev@mlojewski.me> From ecb04a331b5196bde44a74212925326ff1a92075 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Thu, 28 Jun 2018 20:16:43 +0200 Subject: [PATCH 02/30] autocomplete for tables --- js/settings.js | 1 + lib/Controller/SettingsController.php | 3 ++- lib/Platform/AbstractPlatform.php | 13 +++++++++---- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/js/settings.js b/js/settings.js index 87a64d5..47b1061 100644 --- a/js/settings.js +++ b/js/settings.js @@ -41,6 +41,7 @@ user_sql.adminSettingsUI = function () { $(ids).autocomplete({ source: function (request, response) { var post = $(form_id).serializeArray(); + post.push({name: "input", value: request["term"]}); $.post(OC.generateUrl(path), post, response, "json"); }, minLength: 0, diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 7a95d6c..8c4a9fb 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -266,7 +266,8 @@ class SettingsController extends Controller try { $connection = $this->getConnection(); $platform = PlatformFactory::getPlatform($connection); - $tables = $platform->getTables(); + $input = $this->request->getParam("input"); + $tables = $platform->getTables($input); $this->logger->debug( "Returning tableAutocomplete(): count(" . count($tables) . ")", diff --git a/lib/Platform/AbstractPlatform.php b/lib/Platform/AbstractPlatform.php index 63156ff..38d8f61 100644 --- a/lib/Platform/AbstractPlatform.php +++ b/lib/Platform/AbstractPlatform.php @@ -49,12 +49,13 @@ abstract class AbstractPlatform /** * Get all the tables defined in the database. * - * @param bool $schemaPrefix Show schema name in the results. + * @param string $phrase Show only tables containing given phrase. + * @param bool $schemaPrefix Show schema name in the results. * * @return array Array with table names. * @throws DBALException On a database exception. */ - public function getTables($schemaPrefix = false) + public function getTables($phrase = "", $schemaPrefix = false) { $platform = $this->connection->getDatabasePlatform(); @@ -68,13 +69,17 @@ abstract class AbstractPlatform $result = $this->connection->executeQuery($queryTables); while ($row = $result->fetch()) { $name = $this->getTableName($row, $schemaPrefix); - $tables[] = $name; + if (preg_match("/.*$phrase.*/i", $name)) { + $tables[] = $name; + } } $result = $this->connection->executeQuery($queryViews); while ($row = $result->fetch()) { $name = $this->getViewName($row, $schemaPrefix); - $tables[] = $name; + if (preg_match("/.*$phrase.*/i", $name)) { + $tables[] = $name; + } } return $tables; From a257855e07ec81b580f401da65f17420d099fafd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Thu, 28 Jun 2018 20:30:11 +0200 Subject: [PATCH 03/30] autocomplete for columns --- lib/Controller/SettingsController.php | 3 ++- lib/Platform/AbstractPlatform.php | 9 ++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/lib/Controller/SettingsController.php b/lib/Controller/SettingsController.php index 8c4a9fb..a192daf 100644 --- a/lib/Controller/SettingsController.php +++ b/lib/Controller/SettingsController.php @@ -315,7 +315,8 @@ class SettingsController extends Controller $connection = $this->getConnection(); $platform = PlatformFactory::getPlatform($connection); $columns = $platform->getColumns( - $this->request->getParam($table) + $this->request->getParam($table), + $this->request->getParam("input") ); return $columns; diff --git a/lib/Platform/AbstractPlatform.php b/lib/Platform/AbstractPlatform.php index 38d8f61..59052d4 100644 --- a/lib/Platform/AbstractPlatform.php +++ b/lib/Platform/AbstractPlatform.php @@ -108,12 +108,13 @@ abstract class AbstractPlatform /** * Get all the columns defined in the table. * - * @param string $table The table name. + * @param string $table The table name. + * @param string $phrase Show only columns containing given phrase. * * @return array Array with column names. * @throws DBALException On a database exception. */ - public function getColumns($table) + public function getColumns($table, $phrase = "") { $platform = $this->connection->getDatabasePlatform(); $query = $platform->getListTableColumnsSQL($table); @@ -123,7 +124,9 @@ abstract class AbstractPlatform while ($row = $result->fetch()) { $name = $this->getColumnName($row); - $columns[] = $name; + if (preg_match("/.*$phrase.*/i", $name)) { + $columns[] = $name; + } } return $columns; From 434e2777c39ee0c0ff12f3c1e8c172fc47b7207b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Thu, 28 Jun 2018 21:04:15 +0200 Subject: [PATCH 04/30] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3564381..9af8528 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,8 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Fixed +- Table and column autocomplete in settings panel ## [v4.0.0-rc2] ### Added From a2b65f144c51be46335d34d12f3310e3533e4329 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Sat, 30 Jun 2018 13:19:04 +0200 Subject: [PATCH 05/30] Adding SALT from DB and new Algorithm to be connected with HumHub https://github.com/nextcloud/user_sql/pull/42 --- CHANGELOG.md | 2 + README.md | 2 + js/settings.js | 2 +- lib/Backend/UserBackend.php | 16 ++++++-- lib/Constant/DB.php | 1 + lib/Crypto/SHA512Whirlpool.php | 58 ++++++++++++++++++++++++++++ lib/Model/User.php | 4 ++ lib/Query/QueryProvider.php | 4 +- templates/admin.php | 5 ++- tests/Crypto/SHA512WhirlpoolTest.php | 56 +++++++++++++++++++++++++++ 10 files changed, 142 insertions(+), 8 deletions(-) create mode 100644 lib/Crypto/SHA512Whirlpool.php create mode 100644 tests/Crypto/SHA512WhirlpoolTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index 9af8528..cc1c2ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [v4.0.0-rc2] ### Added - User active column +- SHA512 Whirlpool hashing algorithm +- Support for salt column ### Changed - Fixed "Use of undefined constant" error for Argon2 Crypt with PHP below 7.2. diff --git a/README.md b/README.md index fc64f4b..01b9e67 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,7 @@ Name | Description | Details **Display name** | Display name column. | Optional. **Active** | Flag indicating if user can log in. | Optional.
Default: true. **Can change avatar** | Flag indicating if user can change its avatar. | Optional.
Default: false. +**Salt** | Salt which is appended to password when checking or changing the password. | Optional. #### Group table @@ -191,6 +192,7 @@ Standard DES (Crypt) | | yTBnb7ab/N072 Joomla MD5 Encryption | Generates 32 chars salt. | 14d21b49b0f13e2acba962b6b0039edd:haJK0yTvBXTNMh76xwEw5RYEVpJsN8us MD5 | No salt supported. | 5f4dcc3b5aa765d61d8327deb882cf99 SHA1 | No salt supported. | 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8 +SHA512 Whirlpool | No salt supported. | a96b16ebb691dbe968b0d66d0d924cff5cf5de5e0885181d00761d87f295b2bf3d3c66187c050fc01c196ff3acaa48d3561ffd170413346e934a32280d632f2e SSHA256 | Generates 32 chars salt. | {SSHA256}+WxTB3JxprNteeovsuSYtgI+UkVPA9lfwGoYkz3Ff7hjd1FSdmlTMkNsSExyR21KM3NvNTZ5V0p4WXJMUjFzUg== SSHA512 | Generates 32 chars salt. | {SSHA512}It+v1kAEUBbhMJYJ2swAtz+RLE6ispv/FB6G/ALhK/YWwEmrloY+0jzrWIfmu+rWUXp8u0Tg4jLXypC5oXAW00IyYnRVdEZJbE9wak96bkNRVWFCYmlJNWxrdTA0QmhL diff --git a/js/settings.js b/js/settings.js index 47b1061..c42bb4a 100644 --- a/js/settings.js +++ b/js/settings.js @@ -76,7 +76,7 @@ user_sql.adminSettingsUI = function () { ); autocomplete( - "#db-table-user-column-uid, #db-table-user-column-email, #db-table-user-column-home, #db-table-user-column-password, #db-table-user-column-name, #db-table-user-column-active, #db-table-user-column-avatar", + "#db-table-user-column-uid, #db-table-user-column-email, #db-table-user-column-home, #db-table-user-column-password, #db-table-user-column-name, #db-table-user-column-active, #db-table-user-column-avatar, #db-table-user-column-salt", "/apps/user_sql/settings/autocomplete/table/user" ); diff --git a/lib/Backend/UserBackend.php b/lib/Backend/UserBackend.php index 88119ee..6a8237b 100644 --- a/lib/Backend/UserBackend.php +++ b/lib/Backend/UserBackend.php @@ -274,6 +274,10 @@ final class UserBackend extends Backend return false; } + if ($user->salt !== null) { + $password .= $user->salt; + } + $isCorrect = $passwordAlgorithm->checkPassword( $password, $user->password ); @@ -417,13 +421,17 @@ final class UserBackend extends Backend return false; } - $passwordHash = $passwordAlgorithm->getPasswordHash($password); - if ($passwordHash === false) { + $user = $this->userRepository->findByUid($uid); + if (!($user instanceof User)) { return false; } - $user = $this->userRepository->findByUid($uid); - if (!($user instanceof User)) { + if ($user->salt !== null) { + $password .= $user->salt; + } + + $passwordHash = $passwordAlgorithm->getPasswordHash($password); + if ($passwordHash === false) { return false; } diff --git a/lib/Constant/DB.php b/lib/Constant/DB.php index 832cda5..ce0da21 100644 --- a/lib/Constant/DB.php +++ b/lib/Constant/DB.php @@ -51,5 +51,6 @@ final class DB const USER_HOME_COLUMN = "db.table.user.column.home"; const USER_NAME_COLUMN = "db.table.user.column.name"; const USER_PASSWORD_COLUMN = "db.table.user.column.password"; + const USER_SALT_COLUMN = "db.table.user.column.salt"; const USER_UID_COLUMN = "db.table.user.column.uid"; } diff --git a/lib/Crypto/SHA512Whirlpool.php b/lib/Crypto/SHA512Whirlpool.php new file mode 100644 index 0000000..1fd3988 --- /dev/null +++ b/lib/Crypto/SHA512Whirlpool.php @@ -0,0 +1,58 @@ + + * @author Marcin Łojewski + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\UserSQL\Crypto; + +use OCP\IL10N; + +/** + * SHA512 Whirlpool hashing implementation. + * + * @author Marcin Łojewski + */ +class SHA512Whirlpool extends AbstractAlgorithm +{ + /** + * The class constructor. + * + * @param IL10N $localization The localization service. + */ + public function __construct(IL10N $localization) + { + parent::__construct($localization); + } + + /** + * @inheritdoc + */ + public function getPasswordHash($password) + { + return hash('sha512', hash('whirlpool', $password)); + } + + /** + * @inheritdoc + */ + protected function getAlgorithmName() + { + return "SHA512 Whirlpool"; + } +} diff --git a/lib/Model/User.php b/lib/Model/User.php index 90048f9..dcc9551 100644 --- a/lib/Model/User.php +++ b/lib/Model/User.php @@ -56,4 +56,8 @@ class User * @var bool Can user change its avatar. */ public $avatar; + /** + * @var string The password's salt. + */ + public $salt; } diff --git a/lib/Query/QueryProvider.php b/lib/Query/QueryProvider.php index 742e784..49f2ac9 100644 --- a/lib/Query/QueryProvider.php +++ b/lib/Query/QueryProvider.php @@ -71,6 +71,7 @@ class QueryProvider implements \ArrayAccess $uHome = $this->properties[DB::USER_HOME_COLUMN]; $uName = $this->properties[DB::USER_NAME_COLUMN]; $uPassword = $this->properties[DB::USER_PASSWORD_COLUMN]; + $uSalt = $this->properties[DB::USER_SALT_COLUMN]; $uUID = $this->properties[DB::USER_UID_COLUMN]; $ugGID = $this->properties[DB::USER_GROUP_GID_COLUMN]; @@ -92,7 +93,8 @@ class QueryProvider implements \ArrayAccess (empty($uEmail) ? "null" : $uEmail) . " AS email, " . (empty($uHome) ? "null" : $uHome) . " AS home, " . (empty($uActive) ? "true" : $uActive) . " AS active, " . - (empty($uAvatar) ? "false" : $uAvatar) . " AS avatar"; + (empty($uAvatar) ? "false" : $uAvatar) . " AS avatar, " . + (empty($uSalt) ? "null" : $uSalt) . " AS salt"; $this->queries = [ Query::BELONGS_TO_ADMIN => diff --git a/templates/admin.php b/templates/admin.php index 43d812e..a39becf 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -148,7 +148,8 @@ function print_select_options( print_text_input($l, "db-table-user-column-password", "Password", $_['db.table.user.column.password']); print_text_input($l, "db-table-user-column-name", "Display name", $_['db.table.user.column.name']); print_text_input($l, "db-table-user-column-active", "Active", $_['db.table.user.column.active']); - print_text_input($l, "db-table-user-column-avatar", "Can change avatar", $_['db.table.user.column.avatar']); ?> + print_text_input($l, "db-table-user-column-avatar", "Can change avatar", $_['db.table.user.column.avatar']); + print_text_input($l, "db-table-user-column-salt", "Salt", $_['db.table.user.column.salt']); ?>
@@ -180,4 +181,4 @@ function print_select_options(
- \ No newline at end of file + diff --git a/tests/Crypto/SHA512WhirlpoolTest.php b/tests/Crypto/SHA512WhirlpoolTest.php new file mode 100644 index 0000000..558db94 --- /dev/null +++ b/tests/Crypto/SHA512WhirlpoolTest.php @@ -0,0 +1,56 @@ + + * @author Marcin Łojewski + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Tests\UserSQL\Crypto; + +use OCA\UserSQL\Crypto\SHA512Whirlpool; +use OCA\UserSQL\Crypto\IPasswordAlgorithm; +use OCP\IL10N; +use Test\TestCase; + +/** + * Unit tests for class SHA512Whirlpool. + * + * @author Marcin Łojewski + */ +class SHA512WhirlpoolTest extends TestCase +{ + /** + * @var IPasswordAlgorithm + */ + private $crypto; + + public function testCheckPassword() + { + $this->assertTrue( + $this->crypto->checkPassword( + "password", + "a96b16ebb691dbe968b0d66d0d924cff5cf5de5e0885181d00761d87f295b2bf3d3c66187c050fc01c196ff3acaa48d3561ffd170413346e934a32280d632f2e" + ) + ); + } + + protected function setUp() + { + parent::setUp(); + $this->crypto = new SHA512Whirlpool($this->createMock(IL10N::class)); + } +} From ef9db7444ac2e8e7416cc40f076e0d0748b042c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Sat, 30 Jun 2018 14:32:13 +0200 Subject: [PATCH 06/30] Feature/issue#44 (#47) phpass implementation --- CHANGELOG.md | 7 +- README.md | 3 +- lib/Crypto/PasswordHash.php | 230 ++++++++++++++++++++++++++++++++++++ lib/Crypto/Phpass.php | 75 ++++++++++++ tests/Crypto/PhpassTest.php | 55 +++++++++ 5 files changed, 367 insertions(+), 3 deletions(-) create mode 100644 lib/Crypto/PasswordHash.php create mode 100644 lib/Crypto/Phpass.php create mode 100644 tests/Crypto/PhpassTest.php diff --git a/CHANGELOG.md b/CHANGELOG.md index cc1c2ee..95d78e1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,14 +5,17 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). ## [Unreleased] +### Added +- SHA512 Whirlpool hashing algorithm +- phpass hashing implementation +- Support for salt column + ### Fixed - Table and column autocomplete in settings panel ## [v4.0.0-rc2] ### Added - User active column -- SHA512 Whirlpool hashing algorithm -- Support for salt column ### Changed - Fixed "Use of undefined constant" error for Argon2 Crypt with PHP below 7.2. diff --git a/README.md b/README.md index 01b9e67..82bfc17 100644 --- a/README.md +++ b/README.md @@ -149,7 +149,7 @@ User table: wp_users Username column: user_login Password column: user_pass -Hashing algorithm: Unix (Crypt) +Hashing algorithm: Unix (Crypt) or Portable PHP password ``` #### JHipster @@ -191,6 +191,7 @@ SHA512 (Crypt) | Generates hash with 5000 rounds. | $6$rounds=5000$yH.Q0OL4qbCOU Standard DES (Crypt) | | yTBnb7ab/N072 Joomla MD5 Encryption | Generates 32 chars salt. | 14d21b49b0f13e2acba962b6b0039edd:haJK0yTvBXTNMh76xwEw5RYEVpJsN8us MD5 | No salt supported. | 5f4dcc3b5aa765d61d8327deb882cf99 +Portable PHP password | See [phpass](http://www.openwall.com/phpass/). | $P$BxrwraqNTi4as0EI.IpiA/K.muk9ke/ SHA1 | No salt supported. | 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8 SHA512 Whirlpool | No salt supported. | a96b16ebb691dbe968b0d66d0d924cff5cf5de5e0885181d00761d87f295b2bf3d3c66187c050fc01c196ff3acaa48d3561ffd170413346e934a32280d632f2e SSHA256 | Generates 32 chars salt. | {SSHA256}+WxTB3JxprNteeovsuSYtgI+UkVPA9lfwGoYkz3Ff7hjd1FSdmlTMkNsSExyR21KM3NvNTZ5V0p4WXJMUjFzUg== diff --git a/lib/Crypto/PasswordHash.php b/lib/Crypto/PasswordHash.php new file mode 100644 index 0000000..6228ba7 --- /dev/null +++ b/lib/Crypto/PasswordHash.php @@ -0,0 +1,230 @@ + in 2004-2006 and placed in +# the public domain. Revised in subsequent years, still public domain. +# +# There's absolutely no warranty. +# +# The homepage URL for this framework is: +# +# http://www.openwall.com/phpass/ +# +# Please be sure to update the Version line if you edit this file in any way. +# It is suggested that you leave the main version number intact, but indicate +# your project name (after the slash) and add your own revision information. +# +# Please do not change the "private" password hashing method implemented in +# here, thereby making your hashes incompatible. However, if you must, please +# change the hash type identifier (the "$P$") to something different. +# +# Obviously, since this code is in the public domain, the above are not +# requirements (there can be none), but merely suggestions. +# + +namespace OCA\UserSQL\Crypto; + +class PasswordHash { + var $itoa64; + var $iteration_count_log2; + var $portable_hashes; + var $random_state; + + function __construct($iteration_count_log2, $portable_hashes) + { + $this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31) + $iteration_count_log2 = 8; + $this->iteration_count_log2 = $iteration_count_log2; + + $this->portable_hashes = $portable_hashes; + + $this->random_state = microtime(); + if (function_exists('getmypid')) + $this->random_state .= getmypid(); + } + + function PasswordHash($iteration_count_log2, $portable_hashes) + { + self::__construct($iteration_count_log2, $portable_hashes); + } + + function get_random_bytes($count) + { + $output = ''; + if (@is_readable('/dev/urandom') && + ($fh = @fopen('/dev/urandom', 'rb'))) { + $output = fread($fh, $count); + fclose($fh); + } + + if (strlen($output) < $count) { + $output = ''; + for ($i = 0; $i < $count; $i += 16) { + $this->random_state = + md5(microtime() . $this->random_state); + $output .= md5($this->random_state, TRUE); + } + $output = substr($output, 0, $count); + } + + return $output; + } + + function encode64($input, $count) + { + $output = ''; + $i = 0; + do { + $value = ord($input[$i++]); + $output .= $this->itoa64[$value & 0x3f]; + if ($i < $count) + $value |= ord($input[$i]) << 8; + $output .= $this->itoa64[($value >> 6) & 0x3f]; + if ($i++ >= $count) + break; + if ($i < $count) + $value |= ord($input[$i]) << 16; + $output .= $this->itoa64[($value >> 12) & 0x3f]; + if ($i++ >= $count) + break; + $output .= $this->itoa64[($value >> 18) & 0x3f]; + } while ($i < $count); + + return $output; + } + + function gensalt_private($input) + { + $output = '$P$'; + $output .= $this->itoa64[min($this->iteration_count_log2 + + ((PHP_VERSION >= '5') ? 5 : 3), 30)]; + $output .= $this->encode64($input, 6); + + return $output; + } + + function crypt_private($password, $setting) + { + $output = '*0'; + if (substr($setting, 0, 2) === $output) + $output = '*1'; + + $id = substr($setting, 0, 3); + # We use "$P$", phpBB3 uses "$H$" for the same thing + if ($id !== '$P$' && $id !== '$H$') + return $output; + + $count_log2 = strpos($this->itoa64, $setting[3]); + if ($count_log2 < 7 || $count_log2 > 30) + return $output; + + $count = 1 << $count_log2; + + $salt = substr($setting, 4, 8); + if (strlen($salt) !== 8) + return $output; + + # We were kind of forced to use MD5 here since it's the only + # cryptographic primitive that was available in all versions + # of PHP in use. To implement our own low-level crypto in PHP + # would have resulted in much worse performance and + # consequently in lower iteration counts and hashes that are + # quicker to crack (by non-PHP code). + $hash = md5($salt . $password, TRUE); + do { + $hash = md5($hash . $password, TRUE); + } while (--$count); + + $output = substr($setting, 0, 12); + $output .= $this->encode64($hash, 16); + + return $output; + } + + function gensalt_blowfish($input) + { + # This one needs to use a different order of characters and a + # different encoding scheme from the one in encode64() above. + # We care because the last character in our encoded string will + # only represent 2 bits. While two known implementations of + # bcrypt will happily accept and correct a salt string which + # has the 4 unused bits set to non-zero, we do not want to take + # chances and we also do not want to waste an additional byte + # of entropy. + $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; + + $output = '$2a$'; + $output .= chr(ord('0') + $this->iteration_count_log2 / 10); + $output .= chr(ord('0') + $this->iteration_count_log2 % 10); + $output .= '$'; + + $i = 0; + do { + $c1 = ord($input[$i++]); + $output .= $itoa64[$c1 >> 2]; + $c1 = ($c1 & 0x03) << 4; + if ($i >= 16) { + $output .= $itoa64[$c1]; + break; + } + + $c2 = ord($input[$i++]); + $c1 |= $c2 >> 4; + $output .= $itoa64[$c1]; + $c1 = ($c2 & 0x0f) << 2; + + $c2 = ord($input[$i++]); + $c1 |= $c2 >> 6; + $output .= $itoa64[$c1]; + $output .= $itoa64[$c2 & 0x3f]; + } while (1); + + return $output; + } + + function HashPassword($password) + { + $random = ''; + + if (CRYPT_BLOWFISH === 1 && !$this->portable_hashes) { + $random = $this->get_random_bytes(16); + $hash = + crypt($password, $this->gensalt_blowfish($random)); + if (strlen($hash) === 60) + return $hash; + } + + if (strlen($random) < 6) + $random = $this->get_random_bytes(6); + $hash = + $this->crypt_private($password, + $this->gensalt_private($random)); + if (strlen($hash) === 34) + return $hash; + + # Returning '*' on error is safe here, but would _not_ be safe + # in a crypt(3)-like function used _both_ for generating new + # hashes and for validating passwords against existing hashes. + return '*'; + } + + function CheckPassword($password, $stored_hash) + { + $hash = $this->crypt_private($password, $stored_hash); + if ($hash[0] === '*') + $hash = crypt($password, $stored_hash); + + # This is not constant-time. In order to keep the code simple, + # for timing safety we currently rely on the salts being + # unpredictable, which they are at least in the non-fallback + # cases (that is, when we use /dev/urandom and bcrypt). + return $hash === $stored_hash; + } +} + +?> diff --git a/lib/Crypto/Phpass.php b/lib/Crypto/Phpass.php new file mode 100644 index 0000000..1feba29 --- /dev/null +++ b/lib/Crypto/Phpass.php @@ -0,0 +1,75 @@ + + * @author Marcin Łojewski + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\UserSQL\Crypto; + +use OCP\IL10N; + +/** + * phpass hashing implementation. + * + * @author Marcin Łojewski + */ +class Phpass extends AbstractAlgorithm +{ + /** + * @var PasswordHash + */ + private $hasher; + + /** + * The class constructor. + * + * @param IL10N $localization The localization service. + * @param int $hashCostLog2 Log2 Hash cost. + * @param bool $hashPortable Use portable hash implementation. + */ + public function __construct( + IL10N $localization, $hashCostLog2 = 8, $hashPortable = true + ) { + parent::__construct($localization); + $this->hasher = new PasswordHash($hashCostLog2, $hashPortable); + } + + /** + * @inheritdoc + */ + public function checkPassword($password, $dbHash) + { + return $this->hasher->CheckPassword($password, $dbHash); + } + + /** + * @inheritdoc + */ + public function getPasswordHash($password) + { + return $this->hasher->HashPassword($password); + } + + /** + * @inheritdoc + */ + protected function getAlgorithmName() + { + return "Portable PHP password"; + } +} diff --git a/tests/Crypto/PhpassTest.php b/tests/Crypto/PhpassTest.php new file mode 100644 index 0000000..22b1ecc --- /dev/null +++ b/tests/Crypto/PhpassTest.php @@ -0,0 +1,55 @@ + + * @author Marcin Łojewski + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Tests\UserSQL\Crypto; + +use OCA\UserSQL\Crypto\Phpass; +use OCA\UserSQL\Crypto\IPasswordAlgorithm; +use OCP\IL10N; +use Test\TestCase; + +/** + * Unit tests for class PhpassTest. + * + * @author Marcin Łojewski + */ +class PhpassTest extends TestCase +{ + /** + * @var IPasswordAlgorithm + */ + private $crypto; + + public function testCheckPassword() + { + $this->assertTrue( + $this->crypto->checkPassword( + "password", "\$P\$BxrwraqNTi4as0EI.IpiA/K.muk9ke/" + ) + ); + } + + protected function setUp() + { + parent::setUp(); + $this->crypto = new Phpass($this->createMock(IL10N::class)); + } +} From 74bb55534de087268cf4973ca70e146cf37c808e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Sat, 30 Jun 2018 17:07:25 +0200 Subject: [PATCH 07/30] merge phpass class into crypto class --- lib/Crypto/PasswordHash.php | 230 ------------------------------------ lib/Crypto/Phpass.php | 104 ++++++++++++++-- lib/Crypto/Utils.php | 2 +- 3 files changed, 92 insertions(+), 244 deletions(-) delete mode 100644 lib/Crypto/PasswordHash.php diff --git a/lib/Crypto/PasswordHash.php b/lib/Crypto/PasswordHash.php deleted file mode 100644 index 6228ba7..0000000 --- a/lib/Crypto/PasswordHash.php +++ /dev/null @@ -1,230 +0,0 @@ - in 2004-2006 and placed in -# the public domain. Revised in subsequent years, still public domain. -# -# There's absolutely no warranty. -# -# The homepage URL for this framework is: -# -# http://www.openwall.com/phpass/ -# -# Please be sure to update the Version line if you edit this file in any way. -# It is suggested that you leave the main version number intact, but indicate -# your project name (after the slash) and add your own revision information. -# -# Please do not change the "private" password hashing method implemented in -# here, thereby making your hashes incompatible. However, if you must, please -# change the hash type identifier (the "$P$") to something different. -# -# Obviously, since this code is in the public domain, the above are not -# requirements (there can be none), but merely suggestions. -# - -namespace OCA\UserSQL\Crypto; - -class PasswordHash { - var $itoa64; - var $iteration_count_log2; - var $portable_hashes; - var $random_state; - - function __construct($iteration_count_log2, $portable_hashes) - { - $this->itoa64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; - - if ($iteration_count_log2 < 4 || $iteration_count_log2 > 31) - $iteration_count_log2 = 8; - $this->iteration_count_log2 = $iteration_count_log2; - - $this->portable_hashes = $portable_hashes; - - $this->random_state = microtime(); - if (function_exists('getmypid')) - $this->random_state .= getmypid(); - } - - function PasswordHash($iteration_count_log2, $portable_hashes) - { - self::__construct($iteration_count_log2, $portable_hashes); - } - - function get_random_bytes($count) - { - $output = ''; - if (@is_readable('/dev/urandom') && - ($fh = @fopen('/dev/urandom', 'rb'))) { - $output = fread($fh, $count); - fclose($fh); - } - - if (strlen($output) < $count) { - $output = ''; - for ($i = 0; $i < $count; $i += 16) { - $this->random_state = - md5(microtime() . $this->random_state); - $output .= md5($this->random_state, TRUE); - } - $output = substr($output, 0, $count); - } - - return $output; - } - - function encode64($input, $count) - { - $output = ''; - $i = 0; - do { - $value = ord($input[$i++]); - $output .= $this->itoa64[$value & 0x3f]; - if ($i < $count) - $value |= ord($input[$i]) << 8; - $output .= $this->itoa64[($value >> 6) & 0x3f]; - if ($i++ >= $count) - break; - if ($i < $count) - $value |= ord($input[$i]) << 16; - $output .= $this->itoa64[($value >> 12) & 0x3f]; - if ($i++ >= $count) - break; - $output .= $this->itoa64[($value >> 18) & 0x3f]; - } while ($i < $count); - - return $output; - } - - function gensalt_private($input) - { - $output = '$P$'; - $output .= $this->itoa64[min($this->iteration_count_log2 + - ((PHP_VERSION >= '5') ? 5 : 3), 30)]; - $output .= $this->encode64($input, 6); - - return $output; - } - - function crypt_private($password, $setting) - { - $output = '*0'; - if (substr($setting, 0, 2) === $output) - $output = '*1'; - - $id = substr($setting, 0, 3); - # We use "$P$", phpBB3 uses "$H$" for the same thing - if ($id !== '$P$' && $id !== '$H$') - return $output; - - $count_log2 = strpos($this->itoa64, $setting[3]); - if ($count_log2 < 7 || $count_log2 > 30) - return $output; - - $count = 1 << $count_log2; - - $salt = substr($setting, 4, 8); - if (strlen($salt) !== 8) - return $output; - - # We were kind of forced to use MD5 here since it's the only - # cryptographic primitive that was available in all versions - # of PHP in use. To implement our own low-level crypto in PHP - # would have resulted in much worse performance and - # consequently in lower iteration counts and hashes that are - # quicker to crack (by non-PHP code). - $hash = md5($salt . $password, TRUE); - do { - $hash = md5($hash . $password, TRUE); - } while (--$count); - - $output = substr($setting, 0, 12); - $output .= $this->encode64($hash, 16); - - return $output; - } - - function gensalt_blowfish($input) - { - # This one needs to use a different order of characters and a - # different encoding scheme from the one in encode64() above. - # We care because the last character in our encoded string will - # only represent 2 bits. While two known implementations of - # bcrypt will happily accept and correct a salt string which - # has the 4 unused bits set to non-zero, we do not want to take - # chances and we also do not want to waste an additional byte - # of entropy. - $itoa64 = './ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - - $output = '$2a$'; - $output .= chr(ord('0') + $this->iteration_count_log2 / 10); - $output .= chr(ord('0') + $this->iteration_count_log2 % 10); - $output .= '$'; - - $i = 0; - do { - $c1 = ord($input[$i++]); - $output .= $itoa64[$c1 >> 2]; - $c1 = ($c1 & 0x03) << 4; - if ($i >= 16) { - $output .= $itoa64[$c1]; - break; - } - - $c2 = ord($input[$i++]); - $c1 |= $c2 >> 4; - $output .= $itoa64[$c1]; - $c1 = ($c2 & 0x0f) << 2; - - $c2 = ord($input[$i++]); - $c1 |= $c2 >> 6; - $output .= $itoa64[$c1]; - $output .= $itoa64[$c2 & 0x3f]; - } while (1); - - return $output; - } - - function HashPassword($password) - { - $random = ''; - - if (CRYPT_BLOWFISH === 1 && !$this->portable_hashes) { - $random = $this->get_random_bytes(16); - $hash = - crypt($password, $this->gensalt_blowfish($random)); - if (strlen($hash) === 60) - return $hash; - } - - if (strlen($random) < 6) - $random = $this->get_random_bytes(6); - $hash = - $this->crypt_private($password, - $this->gensalt_private($random)); - if (strlen($hash) === 34) - return $hash; - - # Returning '*' on error is safe here, but would _not_ be safe - # in a crypt(3)-like function used _both_ for generating new - # hashes and for validating passwords against existing hashes. - return '*'; - } - - function CheckPassword($password, $stored_hash) - { - $hash = $this->crypt_private($password, $stored_hash); - if ($hash[0] === '*') - $hash = crypt($password, $stored_hash); - - # This is not constant-time. In order to keep the code simple, - # for timing safety we currently rely on the salts being - # unpredictable, which they are at least in the non-fallback - # cases (that is, when we use /dev/urandom and bcrypt). - return $hash === $stored_hash; - } -} - -?> diff --git a/lib/Crypto/Phpass.php b/lib/Crypto/Phpass.php index 1feba29..586658c 100644 --- a/lib/Crypto/Phpass.php +++ b/lib/Crypto/Phpass.php @@ -30,23 +30,21 @@ use OCP\IL10N; */ class Phpass extends AbstractAlgorithm { - /** - * @var PasswordHash - */ - private $hasher; + const ITOA64 = './0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'; + + private $iterationCount; /** * The class constructor. * - * @param IL10N $localization The localization service. - * @param int $hashCostLog2 Log2 Hash cost. - * @param bool $hashPortable Use portable hash implementation. + * @param IL10N $localization The localization service. + * @param int $iterationCount Iteration count (log2). + * This value must be between 4 and 31. */ - public function __construct( - IL10N $localization, $hashCostLog2 = 8, $hashPortable = true - ) { + public function __construct(IL10N $localization, $iterationCount = 8) + { parent::__construct($localization); - $this->hasher = new PasswordHash($hashCostLog2, $hashPortable); + $this->iterationCount = $iterationCount; } /** @@ -54,7 +52,73 @@ class Phpass extends AbstractAlgorithm */ public function checkPassword($password, $dbHash) { - return $this->hasher->CheckPassword($password, $dbHash); + return hash_equals($dbHash, $this->crypt($password, $dbHash)); + } + + /** + * @param string $password Password to encrypt. + * @param string $setting Hash settings. + * + * @return string|null Generated hash. Null on invalid settings. + */ + private function crypt($password, $setting) + { + $countLog2 = strpos(self::ITOA64, $setting[3]); + if ($countLog2 < 7 || $countLog2 > 30) { + return null; + } + + $count = 1 << $countLog2; + + $salt = substr($setting, 4, 8); + if (strlen($salt) !== 8) { + return null; + } + + $hash = md5($salt . $password, true); + do { + $hash = md5($hash . $password, true); + } while (--$count); + + $output = substr($setting, 0, 12); + $output .= $this->encode64($hash, 16); + + return $output; + } + + /** + * Encode binary input to base64 string. + * + * @param string $input Binary data. + * @param int $count Data size. + * + * @return string Base64 encoded data. + */ + private function encode64($input, $count) + { + $output = ''; + $i = 0; + do { + $value = ord($input[$i++]); + $output .= self::ITOA64[$value & 0x3f]; + if ($i < $count) { + $value |= ord($input[$i]) << 8; + } + $output .= self::ITOA64[($value >> 6) & 0x3f]; + if ($i++ >= $count) { + break; + } + if ($i < $count) { + $value |= ord($input[$i]) << 16; + } + $output .= self::ITOA64[($value >> 12) & 0x3f]; + if ($i++ >= $count) { + break; + } + $output .= self::ITOA64[($value >> 18) & 0x3f]; + } while ($i < $count); + + return $output; } /** @@ -62,7 +126,21 @@ class Phpass extends AbstractAlgorithm */ public function getPasswordHash($password) { - return $this->hasher->HashPassword($password); + return $this->crypt($password, $this->genSalt()); + } + + /** + * Generate salt for the hash. + * + * @return string Salt string. + */ + private function genSalt() + { + $output = '$P$'; + $output .= self::ITOA64[min($this->iterationCount + 5, 30)]; + $output .= $this->encode64(random_bytes(6), 6); + + return $output; } /** diff --git a/lib/Crypto/Utils.php b/lib/Crypto/Utils.php index 4bd0651..407f081 100644 --- a/lib/Crypto/Utils.php +++ b/lib/Crypto/Utils.php @@ -56,7 +56,7 @@ final class Utils { $string = ""; for ($idx = 0; $idx != $length; ++$idx) { - $string .= $alphabet[mt_rand(0, strlen($alphabet) - 1)]; + $string .= $alphabet[random_int(0, strlen($alphabet) - 1)]; } return $string; } From a78ab8f4f4796f458a7995cf23a50114d7bb2498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Sat, 30 Jun 2018 18:18:08 +0200 Subject: [PATCH 08/30] Change support version to 14 --- appinfo/info.xml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/appinfo/info.xml b/appinfo/info.xml index 0e5c94b..38207b0 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -10,8 +10,8 @@ 4.0.0-dev agpl - Andreas Böhler <dev (at) aboehler (dot) at> - Marcin Łojewski <dev@mlojewski.me> + Marcin Łojewski + Andreas Böhler UserSQL https://github.com/nextcloud/user_sql/issues https://github.com/nextcloud/user_sql @@ -22,7 +22,7 @@ auth - + \OCA\UserSQL\Settings\Admin From 3546682804d869d34559b3af0ed00c4f622ca601 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Sat, 30 Jun 2018 19:06:52 +0200 Subject: [PATCH 09/30] Group backend for Nextcloud 14 --- lib/Backend/GroupBackend.php | 113 ++++++++++++++++++++++++----------- 1 file changed, 77 insertions(+), 36 deletions(-) diff --git a/lib/Backend/GroupBackend.php b/lib/Backend/GroupBackend.php index 2ef5cd0..3d514cb 100644 --- a/lib/Backend/GroupBackend.php +++ b/lib/Backend/GroupBackend.php @@ -21,12 +21,20 @@ namespace OCA\UserSQL\Backend; -use OC\Group\Backend; use OCA\UserSQL\Cache; use OCA\UserSQL\Constant\DB; use OCA\UserSQL\Model\Group; use OCA\UserSQL\Properties; use OCA\UserSQL\Repository\GroupRepository; +use OCP\Group\Backend\ABackend; +use OCP\Group\Backend\IAddToGroupBackend; +use OCP\Group\Backend\ICountDisabledInGroup; +use OCP\Group\Backend\ICountUsersBackend; +use OCP\Group\Backend\ICreateGroupBackend; +use OCP\Group\Backend\IDeleteGroupBackend; +use OCP\Group\Backend\IGroupDetailsBackend; +use OCP\Group\Backend\IIsAdminBackend; +use OCP\Group\Backend\IRemoveFromGroupBackend; use OCP\ILogger; /** @@ -34,7 +42,15 @@ use OCP\ILogger; * * @author Marcin Łojewski */ -final class GroupBackend extends Backend +final class GroupBackend extends ABackend implements + IAddToGroupBackend, + ICountDisabledInGroup, + ICountUsersBackend, + ICreateGroupBackend, + IDeleteGroupBackend, + IGroupDetailsBackend, + IIsAdminBackend, + IRemoveFromGroupBackend { /** * @var string The application name. @@ -128,14 +144,9 @@ final class GroupBackend extends Backend } /** - * Returns the number of users in given group matching the search term. - * - * @param string $gid The group ID. - * @param string $search The search term. - * - * @return int The number of users in given group matching the search term. + * @inheritdoc */ - public function countUsersInGroup($gid, $search = "") + public function countUsersInGroup(string $gid, string $search = ""): int { $this->logger->debug( "Entering countUsersInGroup($gid, $search)", @@ -355,18 +366,18 @@ final class GroupBackend extends Backend } /** - * Checks if a user is in the admin group. - * - * @param string $uid User ID. - * - * @return bool TRUE if a user is in the admin group, FALSE otherwise. + * @inheritdoc */ - public function isAdmin($uid) + public function isAdmin(string $uid): bool { $this->logger->debug( "Entering isAdmin($uid)", ["app" => $this->appName] ); + if (empty($this->properties[DB::GROUP_ADMIN_COLUMN])) { + return false; + } + $cacheKey = self::class . "admin_" . $uid; $admin = $this->cache->get($cacheKey); @@ -394,18 +405,18 @@ final class GroupBackend extends Backend } /** - * Get associative array of the group details. - * - * @param string $gid The group ID. - * - * @return array Associative array of the group details. + * @inheritdoc */ - public function getGroupDetails($gid) + public function getGroupDetails(string $gid): array { $this->logger->debug( "Entering getGroupDetails($gid)", ["app" => $this->appName] ); + if (empty($this->properties[DB::GROUP_NAME_COLUMN])) { + return []; + } + $group = $this->getGroup($gid); if (!($group instanceof Group)) { @@ -421,21 +432,6 @@ final class GroupBackend extends Backend return $details; } - /** - * @inheritdoc - */ - public function getSupportedActions() - { - $actions = parent::getSupportedActions(); - - $actions &= empty($this->properties[DB::GROUP_ADMIN_COLUMN]) - ? ~Backend::IS_ADMIN : ~0; - $actions &= empty($this->properties[DB::GROUP_NAME_COLUMN]) - ? ~Backend::GROUP_DETAILS : ~0; - - return $actions; - } - /** * Check if this backend is correctly set and can be enabled. * @@ -454,4 +450,49 @@ final class GroupBackend extends Backend && !empty($this->properties[DB::USER_GROUP_GID_COLUMN]) && !empty($this->properties[DB::USER_GROUP_UID_COLUMN]); } + + /** + * @inheritdoc + */ + public function addToGroup(string $uid, string $gid): bool + { + // TODO: Implement addToGroup() method. + return false; + } + + /** + * @inheritdoc + */ + public function countDisabledInGroup(string $gid): int + { + // TODO: Implement countDisabledInGroup() method. + return 0; + } + + /** + * @inheritdoc + */ + public function createGroup(string $gid): bool + { + // TODO: Implement createGroup() method. + return false; + } + + /** + * @inheritdoc + */ + public function deleteGroup(string $gid): bool + { + // TODO: Implement deleteGroup() method. + return false; + } + + /** + * @inheritdoc + */ + public function removeFromGroup(string $uid, string $gid) + { + // TODO: Implement removeFromGroup() method. + return false; + } } From bb8e05b3bea21d4b697580e5d8f885b622976cf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Sat, 30 Jun 2018 22:14:14 +0200 Subject: [PATCH 10/30] Options to enable execute DMLs on tables --- lib/Constant/Opt.php | 3 ++ templates/admin.php | 69 +++++++++++++++++++++++--------------------- 2 files changed, 39 insertions(+), 33 deletions(-) diff --git a/lib/Constant/Opt.php b/lib/Constant/Opt.php index 56ce8b2..cad2be6 100644 --- a/lib/Constant/Opt.php +++ b/lib/Constant/Opt.php @@ -32,6 +32,9 @@ final class Opt const EMAIL_SYNC = "opt.email_sync"; const HOME_LOCATION = "opt.home_location"; const HOME_MODE = "opt.home_mode"; + const MODIFY_GROUP = "opt.modify_group"; + const MODIFY_USER = "opt.modify_user"; + const MODIFY_USER_GROUP = "opt.modify_user_group"; const NAME_CHANGE = "opt.name_change"; const PASSWORD_CHANGE = "opt.password_change"; const USE_CACHE = "opt.use_cache"; diff --git a/templates/admin.php b/templates/admin.php index a39becf..7469d1f 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -21,8 +21,8 @@ use OCP\IL10N; -script('user_sql', 'settings'); -style('user_sql', 'settings'); +script("user_sql", "settings"); +style("user_sql", "settings"); function print_text_input(IL10N $l, $id, $label, $value = "", $type = "text") { @@ -94,11 +94,11 @@ function print_select_options(

t("Define your database connection parameters.")); ?>

"MySQL", "pgsql" => "PostgreSQL"]; - print_select_options($l, "db-driver", "SQL driver", $drivers, $_['db.driver']); - print_text_input($l, "db-hostname", "Hostname", $_['db.hostname']); - print_text_input($l, "db-database", "Database", $_['db.database']); - print_text_input($l, "db-username", "Username", $_['db.username']); - print_text_input($l, "db-password", "Password", $_['db.password'], "password"); ?> + print_select_options($l, "db-driver", "SQL driver", $drivers, $_["db.driver"]); + print_text_input($l, "db-hostname", "Hostname", $_["db.hostname"]); + print_text_input($l, "db-database", "Database", $_["db.database"]); + print_text_input($l, "db-username", "Username", $_["db.username"]); + print_text_input($l, "db-password", "Password", $_["db.password"], "password"); ?>
">
@@ -108,16 +108,16 @@ function print_select_options(

t("Options")); ?>

t("Here are all currently supported options.")); ?>

+ print_checkbox_input($l, "opt-name_change", "Allow display name change", $_["opt.name_change"]); + print_checkbox_input($l, "opt-password_change", "Allow password change", $_["opt.password_change"]); ?>
+ print_checkbox_input($l, "opt-use_cache", "Use cache", $_["opt.use_cache"], false); ?> ">
"None", "initial" => "Synchronise only once", "force_nc"=>"Nextcloud always wins", "force_sql"=>"SQL always wins"], $_['opt.email_sync']); - print_select_options($l, "opt-home_mode", "Home mode", ["" => "Default", "query" => "Query", "static" => "Static"], $_['opt.home_mode']); - print_text_input($l, "opt-home_location", "Home Location", $_['opt.home_location']); ?> + print_select_options($l, "opt-crypto_class", "Hashing algorithm", $hashing, $_["opt.crypto_class"]); + print_select_options($l, "opt-email_sync", "Email sync", ["" => "None", "initial" => "Synchronise only once", "force_nc"=>"Nextcloud always wins", "force_sql"=>"SQL always wins"], $_["opt.email_sync"]); + print_select_options($l, "opt-home_mode", "Home mode", ["" => "Default", "query" => "Query", "static" => "Static"], $_["opt.home_mode"]); + print_text_input($l, "opt-home_location", "Home Location", $_["opt.home_location"]); ?>

t("User table")); ?>

t("Table containing user accounts.")); ?>

+ print_text_input($l, "db-table-user", "Table name", $_["db.table.user"]); + print_checkbox_input($l, "opt-modify_user", "Allow DML statements on table", $_["opt.modify_user"]); ?>

t("Columns")); ?>

+ print_text_input($l, "db-table-user-column-uid", "Username", $_["db.table.user.column.uid"]); + print_text_input($l, "db-table-user-column-email", "Email", $_["db.table.user.column.email"]); + print_text_input($l, "db-table-user-column-home", "Home", $_["db.table.user.column.home"]); + print_text_input($l, "db-table-user-column-password", "Password", $_["db.table.user.column.password"]); + print_text_input($l, "db-table-user-column-name", "Display name", $_["db.table.user.column.name"]); + print_text_input($l, "db-table-user-column-active", "Active", $_["db.table.user.column.active"]); + print_text_input($l, "db-table-user-column-avatar", "Can change avatar", $_["db.table.user.column.avatar"]); + print_text_input($l, "db-table-user-column-salt", "Salt", $_["db.table.user.column.salt"]); ?>

t("Group table")); ?>

t("Group definitions table.")); ?>

+ print_text_input($l, "db-table-group", "Table name", $_["db.table.group"]); + print_checkbox_input($l, "opt-modify_group", "Allow DML statements on table", $_["opt.modify_group"]); ?>

t("Columns")); ?>

+ print_text_input($l, "db-table-group-column-admin", "Is admin", $_["db.table.group.column.admin"]); + print_text_input($l, "db-table-group-column-name", "Display name", $_["db.table.group.column.name"]); + print_text_input($l, "db-table-group-column-gid", "Group name", $_["db.table.group.column.gid"]); ?>

t("User group table")); ?>

t("Associative table which maps users to groups.")); ?>

+ print_text_input($l, "db-table-user_group", "Table name", $_["db.table.user_group"]); + print_checkbox_input($l, "opt-modify_user_group", "Allow DML statements on table", $_["opt.modify_user_group"]); ?>

t("Columns")); ?>

+ print_text_input($l, "db-table-user_group-column-uid", "Username", $_["db.table.user_group.column.uid"]); + print_text_input($l, "db-table-user_group-column-gid", "Group name", $_["db.table.user_group.column.gid"]); ?>
- - + " id="requesttoken"/> + "/>
From 20ffbfcddb2bad7581e6235d33fa47acfc3ccf9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Sat, 30 Jun 2018 22:59:52 +0200 Subject: [PATCH 11/30] Revert previous commit. --- lib/Constant/Opt.php | 3 --- templates/admin.php | 9 +++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/lib/Constant/Opt.php b/lib/Constant/Opt.php index cad2be6..56ce8b2 100644 --- a/lib/Constant/Opt.php +++ b/lib/Constant/Opt.php @@ -32,9 +32,6 @@ final class Opt const EMAIL_SYNC = "opt.email_sync"; const HOME_LOCATION = "opt.home_location"; const HOME_MODE = "opt.home_mode"; - const MODIFY_GROUP = "opt.modify_group"; - const MODIFY_USER = "opt.modify_user"; - const MODIFY_USER_GROUP = "opt.modify_user_group"; const NAME_CHANGE = "opt.name_change"; const PASSWORD_CHANGE = "opt.password_change"; const USE_CACHE = "opt.use_cache"; diff --git a/templates/admin.php b/templates/admin.php index 7469d1f..176871f 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -139,8 +139,7 @@ function print_select_options(

t("User table")); ?>

t("Table containing user accounts.")); ?>

+ print_text_input($l, "db-table-user", "Table name", $_["db.table.user"]); ?>

t("Columns")); ?>

t("Group table")); ?>

t("Group definitions table.")); ?>

+ print_text_input($l, "db-table-group", "Table name", $_["db.table.group"]); ?>

t("Columns")); ?>

t("User group table")); ?>

t("Associative table which maps users to groups.")); ?>

+ print_text_input($l, "db-table-user_group", "Table name", $_["db.table.user_group"]); ?>

t("Columns")); ?>

Date: Sat, 30 Jun 2018 23:02:27 +0200 Subject: [PATCH 12/30] Group Backend read only actions. --- lib/Backend/GroupBackend.php | 57 +----------------------------------- 1 file changed, 1 insertion(+), 56 deletions(-) diff --git a/lib/Backend/GroupBackend.php b/lib/Backend/GroupBackend.php index 3d514cb..c44d359 100644 --- a/lib/Backend/GroupBackend.php +++ b/lib/Backend/GroupBackend.php @@ -27,14 +27,9 @@ use OCA\UserSQL\Model\Group; use OCA\UserSQL\Properties; use OCA\UserSQL\Repository\GroupRepository; use OCP\Group\Backend\ABackend; -use OCP\Group\Backend\IAddToGroupBackend; -use OCP\Group\Backend\ICountDisabledInGroup; use OCP\Group\Backend\ICountUsersBackend; -use OCP\Group\Backend\ICreateGroupBackend; -use OCP\Group\Backend\IDeleteGroupBackend; use OCP\Group\Backend\IGroupDetailsBackend; use OCP\Group\Backend\IIsAdminBackend; -use OCP\Group\Backend\IRemoveFromGroupBackend; use OCP\ILogger; /** @@ -43,14 +38,9 @@ use OCP\ILogger; * @author Marcin Łojewski */ final class GroupBackend extends ABackend implements - IAddToGroupBackend, - ICountDisabledInGroup, ICountUsersBackend, - ICreateGroupBackend, - IDeleteGroupBackend, IGroupDetailsBackend, - IIsAdminBackend, - IRemoveFromGroupBackend + IIsAdminBackend { /** * @var string The application name. @@ -450,49 +440,4 @@ final class GroupBackend extends ABackend implements && !empty($this->properties[DB::USER_GROUP_GID_COLUMN]) && !empty($this->properties[DB::USER_GROUP_UID_COLUMN]); } - - /** - * @inheritdoc - */ - public function addToGroup(string $uid, string $gid): bool - { - // TODO: Implement addToGroup() method. - return false; - } - - /** - * @inheritdoc - */ - public function countDisabledInGroup(string $gid): int - { - // TODO: Implement countDisabledInGroup() method. - return 0; - } - - /** - * @inheritdoc - */ - public function createGroup(string $gid): bool - { - // TODO: Implement createGroup() method. - return false; - } - - /** - * @inheritdoc - */ - public function deleteGroup(string $gid): bool - { - // TODO: Implement deleteGroup() method. - return false; - } - - /** - * @inheritdoc - */ - public function removeFromGroup(string $uid, string $gid) - { - // TODO: Implement removeFromGroup() method. - return false; - } } From e4d962f13942ec74d7c4d73b22638e966f9b2e05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Sat, 30 Jun 2018 23:06:12 +0200 Subject: [PATCH 13/30] Changelog --- CHANGELOG.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95d78e1..079b961 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,10 +10,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - phpass hashing implementation - Support for salt column +### Changed +- Support for Nextcloud 14 only +- Group backend implementation + ### Fixed - Table and column autocomplete in settings panel -## [v4.0.0-rc2] +## [4.0.0-rc2] ### Added - User active column @@ -75,6 +79,6 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Supported version of ownCloud, Nextcloud: ownCloud 10, Nextcloud 12 [Unreleased]: https://github.com/nextcloud/user_sql/compare/v4.0.0-rc2...develop -[v4.0.0-rc2]: https://github.com/nextcloud/user_sql/compare/v4.0.0-rc1...v4.0.0-rc2 +[4.0.0-rc2]: https://github.com/nextcloud/user_sql/compare/v4.0.0-rc1...v4.0.0-rc2 [4.0.0-rc1]: https://github.com/nextcloud/user_sql/compare/v3.1.0...v4.0.0-rc1 [3.1.0]: https://github.com/nextcloud/user_sql/compare/v2.4.0...v3.1.0 From 01a91f54cecf911cdb7359c2e202b613bb145749 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Sat, 30 Jun 2018 23:20:46 +0200 Subject: [PATCH 14/30] Adopt user backend to Nextcloud 14 interfaces --- CHANGELOG.md | 1 + lib/Backend/UserBackend.php | 88 ++++++++++++++++++++++++------------- 2 files changed, 59 insertions(+), 30 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 079b961..9289665 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Changed - Support for Nextcloud 14 only - Group backend implementation +- User backend implementation ### Fixed - Table and column autocomplete in settings panel diff --git a/lib/Backend/UserBackend.php b/lib/Backend/UserBackend.php index 6a8237b..0955ccf 100644 --- a/lib/Backend/UserBackend.php +++ b/lib/Backend/UserBackend.php @@ -21,7 +21,6 @@ namespace OCA\UserSQL\Backend; -use OC\User\Backend; use OCA\UserSQL\Action\EmailSync; use OCA\UserSQL\Action\IUserAction; use OCA\UserSQL\Cache; @@ -35,13 +34,28 @@ use OCA\UserSQL\Repository\UserRepository; use OCP\IConfig; use OCP\IL10N; use OCP\ILogger; +use OCP\User\Backend\ABackend; +use OCP\User\Backend\ICheckPasswordBackend; +use OCP\User\Backend\ICountUsersBackend; +use OCP\User\Backend\IGetDisplayNameBackend; +use OCP\User\Backend\IGetHomeBackend; +use OCP\User\Backend\IProvideAvatarBackend; +use OCP\User\Backend\ISetDisplayNameBackend; +use OCP\User\Backend\ISetPasswordBackend; /** * The SQL user backend manager. * * @author Marcin Łojewski */ -final class UserBackend extends Backend +final class UserBackend extends ABackend implements + ICheckPasswordBackend, + ICountUsersBackend, + IGetDisplayNameBackend, + IGetHomeBackend, + IProvideAvatarBackend, + ISetDisplayNameBackend, + ISetPasswordBackend { /** * @var string The application name. @@ -228,7 +242,7 @@ final class UserBackend extends Backend /** * @inheritdoc */ - public function getDisplayName($uid) + public function getDisplayName($uid): string { $this->logger->debug( "Entering getDisplayName($uid)", ["app" => $this->appName] @@ -258,7 +272,7 @@ final class UserBackend extends Backend * * @return string|bool The user ID on success, false otherwise. */ - public function checkPassword($uid, $password) + public function checkPassword(string $uid, string $password) { $this->logger->debug( "Entering checkPassword($uid, *)", ["app" => $this->appName] @@ -337,6 +351,10 @@ final class UserBackend extends Backend ["app" => $this->appName] ); + if (empty($this->properties[DB::USER_NAME_COLUMN])) { + return false; + } + $users = $this->getUsers($search, $limit, $offset); $names = []; @@ -410,12 +428,16 @@ final class UserBackend extends Backend * * @return bool TRUE if the password has been set, FALSE otherwise. */ - public function setPassword($uid, $password) + public function setPassword(string $uid, string $password): bool { $this->logger->debug( "Entering setPassword($uid, *)", ["app" => "user_sql"] ); + if (empty($this->properties[Opt::PASSWORD_CHANGE])) { + return false; + } + $passwordAlgorithm = $this->getPasswordAlgorithm(); if ($passwordAlgorithm === false) { return false; @@ -452,12 +474,16 @@ final class UserBackend extends Backend /** * @inheritdoc */ - public function getHome($uid) + public function getHome(string $uid) { $this->logger->debug( "Entering getHome($uid)", ["app" => $this->appName] ); + if (empty($this->properties[Opt::HOME_MODE])) { + return false; + } + $home = false; switch ($this->properties[Opt::HOME_MODE]) { case App::HOME_STATIC: @@ -487,12 +513,16 @@ final class UserBackend extends Backend * * @return bool TRUE if the user can change its avatar, FALSE otherwise. */ - public function canChangeAvatar($uid) + public function canChangeAvatar(string $uid): bool { $this->logger->debug( "Entering canChangeAvatar($uid)", ["app" => $this->appName] ); + if (empty($this->properties[DB::USER_AVATAR_COLUMN])) { + return false; + } + $user = $this->userRepository->findByUid($uid); if (!($user instanceof User)) { return false; @@ -515,13 +545,17 @@ final class UserBackend extends Backend * * @return bool TRUE if the password has been set, FALSE otherwise. */ - public function setDisplayName($uid, $displayName) + public function setDisplayName(string $uid, string $displayName): bool { $this->logger->debug( "Entering setDisplayName($uid, $displayName)", ["app" => $this->appName] ); + if (empty($this->properties[Opt::NAME_CHANGE])) { + return false; + } + $user = $this->userRepository->findByUid($uid); if (!($user instanceof User)) { return false; @@ -541,28 +575,6 @@ final class UserBackend extends Backend return false; } - /** - * @inheritdoc - */ - public function getSupportedActions() - { - $actions = parent::getSupportedActions(); - - $actions &= empty($this->properties[DB::USER_NAME_COLUMN]) - ? ~Backend::GET_DISPLAYNAME : ~0; - $actions &= empty($this->properties[Opt::HOME_MODE]) - ? ~Backend::GET_HOME : ~0; - $actions &= empty($this->properties[DB::USER_AVATAR_COLUMN]) - ? ~Backend::PROVIDE_AVATAR : ~0; - $actions &= (!empty($this->properties[DB::USER_NAME_COLUMN]) - && $this->properties[Opt::NAME_CHANGE]) ? ~0 - : ~Backend::SET_DISPLAYNAME; - $actions &= $this->properties[Opt::PASSWORD_CHANGE] ? ~0 - : ~Backend::SET_PASSWORD; - - return $actions; - } - /** * Check if this backend is correctly set and can be enabled. * @@ -580,4 +592,20 @@ final class UserBackend extends Backend && !empty($this->properties[DB::USER_PASSWORD_COLUMN]) && !empty($this->properties[Opt::CRYPTO_CLASS]); } + + /** + * @inheritdoc + */ + public function getBackendName() + { + return "User SQL"; + } + + /** + * @inheritdoc + */ + public function deleteUser($uid) + { + return false; + } } From 657e5a7cedd26aa4513c50f952cf64b7904040fe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Sat, 30 Jun 2018 23:35:16 +0200 Subject: [PATCH 15/30] Handle null value for isAdmin function --- lib/Backend/GroupBackend.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/Backend/GroupBackend.php b/lib/Backend/GroupBackend.php index c44d359..6174b26 100644 --- a/lib/Backend/GroupBackend.php +++ b/lib/Backend/GroupBackend.php @@ -358,13 +358,13 @@ final class GroupBackend extends ABackend implements /** * @inheritdoc */ - public function isAdmin(string $uid): bool + public function isAdmin(string $uid = null): bool { $this->logger->debug( "Entering isAdmin($uid)", ["app" => $this->appName] ); - if (empty($this->properties[DB::GROUP_ADMIN_COLUMN])) { + if (empty($this->properties[DB::GROUP_ADMIN_COLUMN]) || $uid === null) { return false; } From d3f38eed7c0c46e2495de1e9434ed855f6ca6e64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Mon, 2 Jul 2018 21:01:13 +0200 Subject: [PATCH 16/30] WoltLab Community Framework 2.x hash implementation --- lib/Crypto/WCF2.php | 63 +++++++++++++++++++++++++++++++++++++++ tests/Crypto/WCF2Test.php | 62 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 lib/Crypto/WCF2.php create mode 100644 tests/Crypto/WCF2Test.php diff --git a/lib/Crypto/WCF2.php b/lib/Crypto/WCF2.php new file mode 100644 index 0000000..07b1484 --- /dev/null +++ b/lib/Crypto/WCF2.php @@ -0,0 +1,63 @@ + + * @author Marcin Łojewski + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\UserSQL\Crypto; + +/** + * WCF2 hashing implementation. + * + * @author Marcin Łojewski + */ +class WCF2 extends AbstractCrypt +{ + /** + * @inheritdoc + */ + public function checkPassword($password, $dbHash) + { + return hash_equals($dbHash, crypt(crypt($password, $dbHash), $dbHash)); + } + + /** + * @inheritdoc + */ + public function getPasswordHash($password) + { + $salt = $this->getSalt(); + return crypt(crypt($password, $salt), $salt); + } + + /** + * @inheritdoc + */ + protected function getSalt() + { + return "$2a$08$" . Utils::randomString(22, self::SALT_ALPHABET) . "$"; + } + + /** + * @inheritdoc + */ + protected function getAlgorithmName() + { + return "WoltLab Community Framework 2.x"; + } +} diff --git a/tests/Crypto/WCF2Test.php b/tests/Crypto/WCF2Test.php new file mode 100644 index 0000000..91faa91 --- /dev/null +++ b/tests/Crypto/WCF2Test.php @@ -0,0 +1,62 @@ + + * @author Marcin Łojewski + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace Tests\UserSQL\Crypto; + +use OCA\UserSQL\Crypto\IPasswordAlgorithm; +use OCA\UserSQL\Crypto\WCF2; +use OCP\IL10N; +use Test\TestCase; + +/** + * Unit tests for class WCF2. + * + * @author Marcin Łojewski + */ +class WCF2Test extends TestCase +{ + /** + * @var IPasswordAlgorithm + */ + private $crypto; + + public function testCheckPassword() + { + $this->assertTrue( + $this->crypto->checkPassword( + "password", + "$2a$08\$XEQDKNU/Vbootwxv5Gp7gujxFX/RUFsZLvQPYM435Dd3/p17fto02" + ) + ); + } + + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + + protected function setUp() + { + parent::setUp(); + $this->crypto = new WCF2($this->createMock(IL10N::class)); + } +} From 89598a5b367a6ec8e031b8f43efde5fd99fb8c37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Mon, 2 Jul 2018 21:06:20 +0200 Subject: [PATCH 17/30] make getSalt() abstract --- lib/Crypto/AbstractCrypt.php | 5 +---- lib/Crypto/Crypt.php | 8 ++++++++ 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/lib/Crypto/AbstractCrypt.php b/lib/Crypto/AbstractCrypt.php index c13e1b5..7ce1c94 100644 --- a/lib/Crypto/AbstractCrypt.php +++ b/lib/Crypto/AbstractCrypt.php @@ -56,8 +56,5 @@ abstract class AbstractCrypt extends AbstractAlgorithm * * @return string The salt string. */ - protected function getSalt() - { - return ""; - } + protected abstract function getSalt(); } diff --git a/lib/Crypto/Crypt.php b/lib/Crypto/Crypt.php index c52be8d..609bdcb 100644 --- a/lib/Crypto/Crypt.php +++ b/lib/Crypto/Crypt.php @@ -56,4 +56,12 @@ class Crypt extends AbstractCrypt { return "Unix (Crypt)"; } + + /** + * Not used. + */ + protected function getSalt() + { + return null; + } } From 6a01cff0952c665c53c5fe9fbf5f1b3cac9402de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Mon, 2 Jul 2018 21:10:27 +0200 Subject: [PATCH 18/30] Test getPasswordHash() methods --- tests/Crypto/CleartextTest.php | 6 ++++++ tests/Crypto/CourierMD5RawTest.php | 6 ++++++ tests/Crypto/CourierMD5Test.php | 6 ++++++ tests/Crypto/CourierSHA1Test.php | 6 ++++++ tests/Crypto/CourierSHA256Test.php | 6 ++++++ tests/Crypto/CryptArgon2Test.php | 6 ++++++ tests/Crypto/CryptBlowfishTest.php | 6 ++++++ tests/Crypto/CryptExtendedDESTest.php | 6 ++++++ tests/Crypto/CryptMD5Test.php | 6 ++++++ tests/Crypto/CryptSHA256Test.php | 6 ++++++ tests/Crypto/CryptSHA512Test.php | 6 ++++++ tests/Crypto/CryptStandardDESTest.php | 6 ++++++ tests/Crypto/CryptTest.php | 6 ++++++ tests/Crypto/JoomlaTest.php | 6 ++++++ tests/Crypto/MD5Test.php | 6 ++++++ tests/Crypto/PhpassTest.php | 6 ++++++ tests/Crypto/SHA1Test.php | 6 ++++++ tests/Crypto/SHA512WhirlpoolTest.php | 6 ++++++ tests/Crypto/SSHA256Test.php | 6 ++++++ tests/Crypto/SSHA512Test.php | 6 ++++++ 20 files changed, 120 insertions(+) diff --git a/tests/Crypto/CleartextTest.php b/tests/Crypto/CleartextTest.php index d4eaaef..67d5547 100644 --- a/tests/Crypto/CleartextTest.php +++ b/tests/Crypto/CleartextTest.php @@ -43,6 +43,12 @@ class CleartextTest extends TestCase $this->assertTrue($this->crypto->checkPassword("password", "password")); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/CourierMD5RawTest.php b/tests/Crypto/CourierMD5RawTest.php index fe30008..5f443a7 100644 --- a/tests/Crypto/CourierMD5RawTest.php +++ b/tests/Crypto/CourierMD5RawTest.php @@ -47,6 +47,12 @@ class CourierMD5RawTest extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/CourierMD5Test.php b/tests/Crypto/CourierMD5Test.php index 0d1e82d..66d3d82 100644 --- a/tests/Crypto/CourierMD5Test.php +++ b/tests/Crypto/CourierMD5Test.php @@ -47,6 +47,12 @@ class CourierMD5Test extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/CourierSHA1Test.php b/tests/Crypto/CourierSHA1Test.php index 0621655..b60c3bc 100644 --- a/tests/Crypto/CourierSHA1Test.php +++ b/tests/Crypto/CourierSHA1Test.php @@ -47,6 +47,12 @@ class CourierSHA1Test extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/CourierSHA256Test.php b/tests/Crypto/CourierSHA256Test.php index ee86310..05cebe8 100644 --- a/tests/Crypto/CourierSHA256Test.php +++ b/tests/Crypto/CourierSHA256Test.php @@ -48,6 +48,12 @@ class CourierSHA256Test extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/CryptArgon2Test.php b/tests/Crypto/CryptArgon2Test.php index 7a44ddd..39855fe 100644 --- a/tests/Crypto/CryptArgon2Test.php +++ b/tests/Crypto/CryptArgon2Test.php @@ -48,6 +48,12 @@ class CryptArgon2Test extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/CryptBlowfishTest.php b/tests/Crypto/CryptBlowfishTest.php index ea4dc0c..1b878fd 100644 --- a/tests/Crypto/CryptBlowfishTest.php +++ b/tests/Crypto/CryptBlowfishTest.php @@ -48,6 +48,12 @@ class CryptBlowfishTest extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/CryptExtendedDESTest.php b/tests/Crypto/CryptExtendedDESTest.php index 31ca7c1..c5627e0 100644 --- a/tests/Crypto/CryptExtendedDESTest.php +++ b/tests/Crypto/CryptExtendedDESTest.php @@ -45,6 +45,12 @@ class CryptExtendedDESTest extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/CryptMD5Test.php b/tests/Crypto/CryptMD5Test.php index 0a6f405..aadc429 100644 --- a/tests/Crypto/CryptMD5Test.php +++ b/tests/Crypto/CryptMD5Test.php @@ -47,6 +47,12 @@ class CryptMD5Test extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/CryptSHA256Test.php b/tests/Crypto/CryptSHA256Test.php index 020bb61..b40736d 100644 --- a/tests/Crypto/CryptSHA256Test.php +++ b/tests/Crypto/CryptSHA256Test.php @@ -48,6 +48,12 @@ class CryptSHA256Test extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/CryptSHA512Test.php b/tests/Crypto/CryptSHA512Test.php index 7667d1f..6ea1035 100644 --- a/tests/Crypto/CryptSHA512Test.php +++ b/tests/Crypto/CryptSHA512Test.php @@ -48,6 +48,12 @@ class CryptSHA512Test extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/CryptStandardDESTest.php b/tests/Crypto/CryptStandardDESTest.php index ca8712b..b8d8cbf 100644 --- a/tests/Crypto/CryptStandardDESTest.php +++ b/tests/Crypto/CryptStandardDESTest.php @@ -45,6 +45,12 @@ class CryptStandardDESTest extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/CryptTest.php b/tests/Crypto/CryptTest.php index f556289..680b6cc 100644 --- a/tests/Crypto/CryptTest.php +++ b/tests/Crypto/CryptTest.php @@ -48,6 +48,12 @@ class CryptTest extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/JoomlaTest.php b/tests/Crypto/JoomlaTest.php index feaa96e..0777c3d 100644 --- a/tests/Crypto/JoomlaTest.php +++ b/tests/Crypto/JoomlaTest.php @@ -48,6 +48,12 @@ class JoomlaTest extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/MD5Test.php b/tests/Crypto/MD5Test.php index d8f2950..d302752 100644 --- a/tests/Crypto/MD5Test.php +++ b/tests/Crypto/MD5Test.php @@ -47,6 +47,12 @@ class MD5Test extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/PhpassTest.php b/tests/Crypto/PhpassTest.php index 22b1ecc..6ef9c42 100644 --- a/tests/Crypto/PhpassTest.php +++ b/tests/Crypto/PhpassTest.php @@ -47,6 +47,12 @@ class PhpassTest extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/SHA1Test.php b/tests/Crypto/SHA1Test.php index 2ed51ab..bdee003 100644 --- a/tests/Crypto/SHA1Test.php +++ b/tests/Crypto/SHA1Test.php @@ -47,6 +47,12 @@ class SHA1Test extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/SHA512WhirlpoolTest.php b/tests/Crypto/SHA512WhirlpoolTest.php index 558db94..3239776 100644 --- a/tests/Crypto/SHA512WhirlpoolTest.php +++ b/tests/Crypto/SHA512WhirlpoolTest.php @@ -48,6 +48,12 @@ class SHA512WhirlpoolTest extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/SSHA256Test.php b/tests/Crypto/SSHA256Test.php index f26b0e7..e3189d0 100644 --- a/tests/Crypto/SSHA256Test.php +++ b/tests/Crypto/SSHA256Test.php @@ -48,6 +48,12 @@ class SSHA256Test extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); diff --git a/tests/Crypto/SSHA512Test.php b/tests/Crypto/SSHA512Test.php index 10cfbd7..b6b5f72 100644 --- a/tests/Crypto/SSHA512Test.php +++ b/tests/Crypto/SSHA512Test.php @@ -48,6 +48,12 @@ class SSHA512Test extends TestCase ); } + public function testPasswordHash() + { + $hash = $this->crypto->getPasswordHash("password"); + $this->assertTrue($this->crypto->checkPassword("password", $hash)); + } + protected function setUp() { parent::setUp(); From 90dde69ec9d3f116f0b2a39c66a49caaac142438 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Mon, 2 Jul 2018 21:11:34 +0200 Subject: [PATCH 19/30] changelog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95d78e1..fe5750c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added - SHA512 Whirlpool hashing algorithm +- WoltLab Community Framework 2.x hashing algorithm - phpass hashing implementation - Support for salt column From edc3e8fe1487c333f86001d55d61969c95e7cd9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Tue, 3 Jul 2018 10:01:30 +0200 Subject: [PATCH 20/30] Descpription of WoltLab Community Framework 2.x --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 82bfc17..d333a5d 100644 --- a/README.md +++ b/README.md @@ -196,6 +196,7 @@ SHA1 | No salt supported. | 5baa61e4c9b93f3f0682250b6cf8331b7ee68fd8 SHA512 Whirlpool | No salt supported. | a96b16ebb691dbe968b0d66d0d924cff5cf5de5e0885181d00761d87f295b2bf3d3c66187c050fc01c196ff3acaa48d3561ffd170413346e934a32280d632f2e SSHA256 | Generates 32 chars salt. | {SSHA256}+WxTB3JxprNteeovsuSYtgI+UkVPA9lfwGoYkz3Ff7hjd1FSdmlTMkNsSExyR21KM3NvNTZ5V0p4WXJMUjFzUg== SSHA512 | Generates 32 chars salt. | {SSHA512}It+v1kAEUBbhMJYJ2swAtz+RLE6ispv/FB6G/ALhK/YWwEmrloY+0jzrWIfmu+rWUXp8u0Tg4jLXypC5oXAW00IyYnRVdEZJbE9wak96bkNRVWFCYmlJNWxrdTA0QmhL +WoltLab Community Framework 2.x | Double salted bcrypt. | $2a$08$XEQDKNU/Vbootwxv5Gp7gujxFX/RUFsZLvQPYM435Dd3/p17fto02 ## Development From 9cc09bd96ba931bc3676c9d270fd4fbe403a4531 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Tue, 3 Jul 2018 10:24:32 +0200 Subject: [PATCH 21/30] Update example sql script --- README.md | 25 +++++++++++-------------- 1 file changed, 11 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index d333a5d..d1aee0c 100644 --- a/README.md +++ b/README.md @@ -106,36 +106,33 @@ but be aware that some functionalities requires data changes (update queries). If you don't have any database model yet you can use below tables (MySQL): ``` -CREATE TABLE sql_users +CREATE TABLE sql_user ( - id INT AUTO_INCREMENT PRIMARY KEY, - username VARCHAR(16) NOT NULL, + username VARCHAR(16) PRIMARY KEY, display_name TEXT NULL, email TEXT NULL, home TEXT NULL, password TEXT NOT NULL, active TINYINT(1) NOT NULL DEFAULT '1', - can_change_avatar BOOLEAN NOT NULL DEFAULT FALSE, - CONSTRAINT users_username_uindex UNIQUE (username) + can_change_avatar BOOLEAN NOT NULL DEFAULT FALSE ); CREATE TABLE sql_group ( - id INT AUTO_INCREMENT PRIMARY KEY, - name VARCHAR(16) NOT NULL, + name VARCHAR(16) PRIMARY KEY, display_name TEXT NULL, - admin BOOLEAN NOT NULL DEFAULT FALSE, - CONSTRAINT group_name_uindex UNIQUE (name) + admin BOOLEAN NOT NULL DEFAULT FALSE ); CREATE TABLE sql_user_group ( - id INT AUTO_INCREMENT PRIMARY KEY, - group_name VARCHAR(16) NOT NULL, username VARCHAR(16) NOT NULL, - CONSTRAINT user_group_group_name_username_uindex UNIQUE (group_name, username), - INDEX user_group_group_name_index (group_name), - INDEX user_group_username_index (username) + group_name VARCHAR(16) NOT NULL, + PRIMARY KEY (username, group_name), + FOREIGN KEY (username) REFERENCES sql_user (username), + FOREIGN KEY (group_name) REFERENCES sql_group (name), + INDEX sql_user_group_username_idx (username), + INDEX sql_user_group_group_name_idx (group_name) ); ``` From 859014b083151e7cbfb1d8eddf17317eedac8feb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Tue, 3 Jul 2018 10:35:12 +0200 Subject: [PATCH 22/30] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index fe5750c..8a7d79d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,8 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - WoltLab Community Framework 2.x hashing algorithm - phpass hashing implementation - Support for salt column +### Changed +- Example SQL script in README file ### Fixed - Table and column autocomplete in settings panel @@ -36,7 +38,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - The whole core implementation, which is NOT COMPATIBLE with the previous versions. - Minimum supported PHP version - 7.0 -## Removed +### Removed - MySQL ENCRYPT() hashing implementation - Function is deprecated as of MySQL 5.7.6 and will be removed in a future MySQL release. - MySQL PASSWORD() hashing implementation - Function is deprecated as of MySQL 5.7.6 and will be removed in a future MySQL release. - Redmine hashing implementation - Cannot implement in new core system. From bd74d7f79e131a6b164ca5a49a6aee12807a1530 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Tue, 3 Jul 2018 10:41:34 +0200 Subject: [PATCH 23/30] Update CHANGELOG.md --- CHANGELOG.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a7d79d..6ce96db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - WoltLab Community Framework 2.x hashing algorithm - phpass hashing implementation - Support for salt column + ### Changed - Example SQL script in README file @@ -25,9 +26,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [4.0.0-rc1] ### Added -- New hashing algorithms: Argon2 Crypt (PHP 7.2 and above), Blowfish Crypt, Courier base64-encoded MD5, Courier base64-encoded SHA1, - Courier base64-encoded SHA256, Courier hexadecimal MD5, Extended DES Crypt, SHA256 Crypt, - SHA512 Crypt, SSHA512, Standard DES Crypt +- New hashing algorithms: Argon2 Crypt (PHP 7.2 and above), Blowfish Crypt, Courier base64-encoded MD5, Courier base64-encoded SHA1, Courier base64-encoded SHA256, Courier hexadecimal MD5, Extended DES Crypt, SHA256 Crypt, SHA512 Crypt, SSHA512, Standard DES Crypt - Option to allow users to change their display names - Option to allow user to change its avatar - Database query results cache From 874564d315ebb90fb70c0af19167532a072542f2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Tue, 3 Jul 2018 10:48:23 +0200 Subject: [PATCH 24/30] Update dates --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ce96db..17edfd4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,14 +17,14 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Fixed - Table and column autocomplete in settings panel -## [v4.0.0-rc2] +## [v4.0.0-rc2] - 2018-06-14 ### Added - User active column ### Changed - Fixed "Use of undefined constant" error for Argon2 Crypt with PHP below 7.2. -## [4.0.0-rc1] +## [4.0.0-rc1] - 2018-06-13 ### Added - New hashing algorithms: Argon2 Crypt (PHP 7.2 and above), Blowfish Crypt, Courier base64-encoded MD5, Courier base64-encoded SHA1, Courier base64-encoded SHA256, Courier hexadecimal MD5, Extended DES Crypt, SHA256 Crypt, SHA512 Crypt, SSHA512, Standard DES Crypt - Option to allow users to change their display names From 5c702edee7b1edce6930f899f287b1a89b7e3905 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Mon, 2 Jul 2018 21:59:42 +0200 Subject: [PATCH 25/30] hashing -> hash --- CHANGELOG.md | 14 +++++++------- README.md | 8 ++++---- lib/Crypto/AbstractCrypt.php | 6 +++--- lib/Crypto/CourierMD5.php | 2 +- lib/Crypto/CourierMD5Raw.php | 2 +- lib/Crypto/CourierSHA1.php | 2 +- lib/Crypto/CourierSHA256.php | 2 +- lib/Crypto/Crypt.php | 2 +- lib/Crypto/CryptArgon2.php | 2 +- lib/Crypto/CryptBlowfish.php | 2 +- lib/Crypto/CryptExtendedDES.php | 2 +- lib/Crypto/CryptMD5.php | 2 +- lib/Crypto/CryptSHA256.php | 2 +- lib/Crypto/CryptSHA512.php | 2 +- lib/Crypto/CryptStandardDES.php | 2 +- lib/Crypto/Joomla.php | 2 +- lib/Crypto/MD5.php | 2 +- lib/Crypto/Phpass.php | 2 +- lib/Crypto/SHA1.php | 2 +- lib/Crypto/SHA512Whirlpool.php | 2 +- lib/Crypto/SSHA.php | 2 +- lib/Crypto/SSHA256.php | 2 +- lib/Crypto/SSHA512.php | 2 +- lib/Crypto/WCF2.php | 2 +- templates/admin.php | 6 +++--- 25 files changed, 38 insertions(+), 38 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17edfd4..b676f34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,9 +6,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [Unreleased] ### Added -- SHA512 Whirlpool hashing algorithm -- WoltLab Community Framework 2.x hashing algorithm -- phpass hashing implementation +- SHA512 Whirlpool hash algorithm +- WoltLab Community Framework 2.x hash algorithm +- phpass hash implementation - Support for salt column ### Changed @@ -26,7 +26,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ## [4.0.0-rc1] - 2018-06-13 ### Added -- New hashing algorithms: Argon2 Crypt (PHP 7.2 and above), Blowfish Crypt, Courier base64-encoded MD5, Courier base64-encoded SHA1, Courier base64-encoded SHA256, Courier hexadecimal MD5, Extended DES Crypt, SHA256 Crypt, SHA512 Crypt, SSHA512, Standard DES Crypt +- New hash algorithms: Argon2 Crypt (PHP 7.2 and above), Blowfish Crypt, Courier base64-encoded MD5, Courier base64-encoded SHA1, Courier base64-encoded SHA256, Courier hexadecimal MD5, Extended DES Crypt, SHA256 Crypt, SHA512 Crypt, SSHA512, Standard DES Crypt - Option to allow users to change their display names - Option to allow user to change its avatar - Database query results cache @@ -38,9 +38,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - Minimum supported PHP version - 7.0 ### Removed -- MySQL ENCRYPT() hashing implementation - Function is deprecated as of MySQL 5.7.6 and will be removed in a future MySQL release. -- MySQL PASSWORD() hashing implementation - Function is deprecated as of MySQL 5.7.6 and will be removed in a future MySQL release. -- Redmine hashing implementation - Cannot implement in new core system. +- MySQL ENCRYPT() hash implementation - Function is deprecated as of MySQL 5.7.6 and will be removed in a future MySQL release. +- MySQL PASSWORD() hash implementation - Function is deprecated as of MySQL 5.7.6 and will be removed in a future MySQL release. +- Redmine hash implementation - Cannot implement in new core system. - User active column - Use database view instead - Domain support diff --git a/README.md b/README.md index d1aee0c..536c6cd 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,7 @@ Name | Description | Details **Allow display name change** | With this option enabled user can change its display name. The display name change is propagated to the database. | Optional.
Default: false.
Requires: user *Display name* column. **Allow password change** | Can user change its password. The password change is propagated to the database. See [Hash algorithms](#hash-algorithms). | Optional.
Default: false. **Use cache** | Use database query results cache. The cache can be cleared any time with the *Clear cache* button click. | Optional.
Default: false. -**Hashing algorithm** | How users passwords are stored in the database. See [Hash algorithms](#hash-algorithms). | Mandatory. +**Hash algorithm** | How users passwords are stored in the database. See [Hash algorithms](#hash-algorithms). | Mandatory. **Email sync** | Sync e-mail address with the Nextcloud.
- *None* - Disables this feature. This is the default option.
- *Synchronise only once* - Copy the e-mail address to the Nextcloud storage if its not set.
- *Nextcloud always wins* - Always copy the e-mail address to the database. This updates the user table.
- *SQL always wins* - Always copy the e-mail address to the Nextcloud storage. | Optional.
Default: *None*.
Requires: user *Email* column. **Home mode** | User storage path.
- *Default* - Let the Nextcloud manage this. The default option.
- *Query* - Use location from the user table pointed by the *home* column.
- *Static* - Use static location. The `%u` variable is replaced with the username of the user. | Optional
Default: *Default*. **Home Location** | User storage path for the `static` *home mode*. | Mandatory if the *Home mode* is set to `Static`. @@ -146,7 +146,7 @@ User table: wp_users Username column: user_login Password column: user_pass -Hashing algorithm: Unix (Crypt) or Portable PHP password +Hash algorithm: Unix (Crypt) or Portable PHP password ``` #### JHipster @@ -163,7 +163,7 @@ Password column: password_hash Email column: email Active column: activated -Hashing algorithm: Unix (Crypt) +Hash algorithm: Unix (Crypt) ``` ## Hash algorithms @@ -203,7 +203,7 @@ Add a new class in the `OCA\UserSQL\Platform` namespace which extends the `Abstr Add this driver in `admin.php` template to `$drivers` variable and in method `getPlatform(Connection $connection)` of `PlatformFactory` class. -#### New hashing algorithm support +#### New hash algorithm support Create a new class in `OCA\UserSQL\Crypto` namespace which implements `IPasswordAlgorithm` interface. Do not forget to write unit tests. diff --git a/lib/Crypto/AbstractCrypt.php b/lib/Crypto/AbstractCrypt.php index 7ce1c94..e27e957 100644 --- a/lib/Crypto/AbstractCrypt.php +++ b/lib/Crypto/AbstractCrypt.php @@ -22,8 +22,8 @@ namespace OCA\UserSQL\Crypto; /** - * Abstract Unix Crypt hashing implementation. - * The hashing algorithm depends on the chosen salt. + * Abstract Unix Crypt hash implementation. + * The hash algorithm depends on the chosen salt. * * @see crypt() * @author Marcin Łojewski @@ -52,7 +52,7 @@ abstract class AbstractCrypt extends AbstractAlgorithm } /** - * Generate a salt string for the hashing algorithm. + * Generate a salt string for the hash algorithm. * * @return string The salt string. */ diff --git a/lib/Crypto/CourierMD5.php b/lib/Crypto/CourierMD5.php index 6e8e71f..c2463e3 100644 --- a/lib/Crypto/CourierMD5.php +++ b/lib/Crypto/CourierMD5.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * Courier MD5 hashing implementation. + * Courier MD5 hash implementation. * * @author Marcin Łojewski */ diff --git a/lib/Crypto/CourierMD5Raw.php b/lib/Crypto/CourierMD5Raw.php index 39fd3db..094eab3 100644 --- a/lib/Crypto/CourierMD5Raw.php +++ b/lib/Crypto/CourierMD5Raw.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * Courier MD5 RAW hashing implementation. + * Courier MD5 RAW hash implementation. * * @author Marcin Łojewski */ diff --git a/lib/Crypto/CourierSHA1.php b/lib/Crypto/CourierSHA1.php index 15d2ef3..6a96a44 100644 --- a/lib/Crypto/CourierSHA1.php +++ b/lib/Crypto/CourierSHA1.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * Courier SHA1 hashing implementation. + * Courier SHA1 hash implementation. * * @author Marcin Łojewski */ diff --git a/lib/Crypto/CourierSHA256.php b/lib/Crypto/CourierSHA256.php index 3bf0ed6..081cd9d 100644 --- a/lib/Crypto/CourierSHA256.php +++ b/lib/Crypto/CourierSHA256.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * Courier SHA256 hashing implementation. + * Courier SHA256 hash implementation. * * @author Marcin Łojewski */ diff --git a/lib/Crypto/Crypt.php b/lib/Crypto/Crypt.php index 609bdcb..c28763a 100644 --- a/lib/Crypto/Crypt.php +++ b/lib/Crypto/Crypt.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * Unix Crypt hashing implementation. + * Unix Crypt hash implementation. * * @see crypt() * @author Marcin Łojewski diff --git a/lib/Crypto/CryptArgon2.php b/lib/Crypto/CryptArgon2.php index 14efb64..ed4aafb 100644 --- a/lib/Crypto/CryptArgon2.php +++ b/lib/Crypto/CryptArgon2.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * Argon2 Crypt hashing implementation. + * Argon2 Crypt hash implementation. * * @see crypt() * @author Marcin Łojewski diff --git a/lib/Crypto/CryptBlowfish.php b/lib/Crypto/CryptBlowfish.php index 8e4a35e..6e1b8a5 100644 --- a/lib/Crypto/CryptBlowfish.php +++ b/lib/Crypto/CryptBlowfish.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * Blowfish Crypt hashing implementation. + * Blowfish Crypt hash implementation. * * @see crypt() * @author Marcin Łojewski diff --git a/lib/Crypto/CryptExtendedDES.php b/lib/Crypto/CryptExtendedDES.php index b09baab..d6654c4 100644 --- a/lib/Crypto/CryptExtendedDES.php +++ b/lib/Crypto/CryptExtendedDES.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * Extended DES Crypt hashing implementation. + * Extended DES Crypt hash implementation. * * @see crypt() * @author Marcin Łojewski diff --git a/lib/Crypto/CryptMD5.php b/lib/Crypto/CryptMD5.php index 6ca2e3b..0211f7e 100644 --- a/lib/Crypto/CryptMD5.php +++ b/lib/Crypto/CryptMD5.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * MD5 Crypt hashing implementation. + * MD5 Crypt hash implementation. * * @see crypt() * @author Marcin Łojewski diff --git a/lib/Crypto/CryptSHA256.php b/lib/Crypto/CryptSHA256.php index fad91b3..b4e2b41 100644 --- a/lib/Crypto/CryptSHA256.php +++ b/lib/Crypto/CryptSHA256.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * SHA256 Crypt hashing implementation. + * SHA256 Crypt hash implementation. * * @see crypt() * @author Marcin Łojewski diff --git a/lib/Crypto/CryptSHA512.php b/lib/Crypto/CryptSHA512.php index 11f3b8f..e32238f 100644 --- a/lib/Crypto/CryptSHA512.php +++ b/lib/Crypto/CryptSHA512.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * SHA512 Crypt hashing implementation. + * SHA512 Crypt hash implementation. * * @see crypt() * @author Marcin Łojewski diff --git a/lib/Crypto/CryptStandardDES.php b/lib/Crypto/CryptStandardDES.php index 7d8fa7d..bf2b4ec 100644 --- a/lib/Crypto/CryptStandardDES.php +++ b/lib/Crypto/CryptStandardDES.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * Standard DES Crypt hashing implementation. + * Standard DES Crypt hash implementation. * * @author Marcin Łojewski */ diff --git a/lib/Crypto/Joomla.php b/lib/Crypto/Joomla.php index e5dd2ca..46af41c 100644 --- a/lib/Crypto/Joomla.php +++ b/lib/Crypto/Joomla.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * Joomla hashing implementation. + * Joomla hash implementation. * * @author Marcin Łojewski */ diff --git a/lib/Crypto/MD5.php b/lib/Crypto/MD5.php index a4ba435..b995b9c 100644 --- a/lib/Crypto/MD5.php +++ b/lib/Crypto/MD5.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * MD5 hashing implementation. + * MD5 hash implementation. * * @author Marcin Łojewski */ diff --git a/lib/Crypto/Phpass.php b/lib/Crypto/Phpass.php index 586658c..d193917 100644 --- a/lib/Crypto/Phpass.php +++ b/lib/Crypto/Phpass.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * phpass hashing implementation. + * phpass hash implementation. * * @author Marcin Łojewski */ diff --git a/lib/Crypto/SHA1.php b/lib/Crypto/SHA1.php index a534212..6a1c707 100644 --- a/lib/Crypto/SHA1.php +++ b/lib/Crypto/SHA1.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * SHA1 hashing implementation. + * SHA1 hash implementation. * * @author Marcin Łojewski */ diff --git a/lib/Crypto/SHA512Whirlpool.php b/lib/Crypto/SHA512Whirlpool.php index 1fd3988..4f36e9a 100644 --- a/lib/Crypto/SHA512Whirlpool.php +++ b/lib/Crypto/SHA512Whirlpool.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * SHA512 Whirlpool hashing implementation. + * SHA512 Whirlpool hash implementation. * * @author Marcin Łojewski */ diff --git a/lib/Crypto/SSHA.php b/lib/Crypto/SSHA.php index f0f46d9..ddae4b2 100644 --- a/lib/Crypto/SSHA.php +++ b/lib/Crypto/SSHA.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * SSHA* hashing implementation. + * SSHA* hash implementation. * * @author Marcin Łojewski */ diff --git a/lib/Crypto/SSHA256.php b/lib/Crypto/SSHA256.php index a01cdf9..40337bd 100644 --- a/lib/Crypto/SSHA256.php +++ b/lib/Crypto/SSHA256.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * SSHA256 hashing implementation. + * SSHA256 hash implementation. * * @author Marcin Łojewski */ diff --git a/lib/Crypto/SSHA512.php b/lib/Crypto/SSHA512.php index 78e66a1..508fe53 100644 --- a/lib/Crypto/SSHA512.php +++ b/lib/Crypto/SSHA512.php @@ -24,7 +24,7 @@ namespace OCA\UserSQL\Crypto; use OCP\IL10N; /** - * SSHA512 hashing implementation. + * SSHA512 hash implementation. * * @author Marcin Łojewski */ diff --git a/lib/Crypto/WCF2.php b/lib/Crypto/WCF2.php index 07b1484..498818a 100644 --- a/lib/Crypto/WCF2.php +++ b/lib/Crypto/WCF2.php @@ -22,7 +22,7 @@ namespace OCA\UserSQL\Crypto; /** - * WCF2 hashing implementation. + * WCF2 hash implementation. * * @author Marcin Łojewski */ diff --git a/templates/admin.php b/templates/admin.php index a39becf..62e0d39 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -115,7 +115,7 @@ function print_select_options( "> getVisibleName(); + $hashes[$class] = $passwordAlgorithm->getVisibleName(); } } catch (Throwable $e) { } } - print_select_options($l, "opt-crypto_class", "Hashing algorithm", $hashing, $_['opt.crypto_class']); + print_select_options($l, "opt-crypto_class", "Hash algorithm", $hashes, $_['opt.crypto_class']); print_select_options($l, "opt-email_sync", "Email sync", ["" => "None", "initial" => "Synchronise only once", "force_nc"=>"Nextcloud always wins", "force_sql"=>"SQL always wins"], $_['opt.email_sync']); print_select_options($l, "opt-home_mode", "Home mode", ["" => "Default", "query" => "Query", "static" => "Static"], $_['opt.home_mode']); print_text_input($l, "opt-home_location", "Home Location", $_['opt.home_location']); ?> From 49a9b2ed61eb002fdfbcdfb8efd200da8cb753a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Sat, 7 Jul 2018 09:27:20 +0200 Subject: [PATCH 26/30] can change avatar -> provide avatar --- README.md | 16 ++++++++-------- templates/admin.php | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/README.md b/README.md index 536c6cd..f2ea96f 100644 --- a/README.md +++ b/README.md @@ -68,7 +68,7 @@ Name | Description | Details **Password** | Password hash column. | Mandatory for user backend. **Display name** | Display name column. | Optional. **Active** | Flag indicating if user can log in. | Optional.
Default: true. -**Can change avatar** | Flag indicating if user can change its avatar. | Optional.
Default: false. +**Provide avatar** | Flag indicating if user can change its avatar. | Optional.
Default: false. **Salt** | Salt which is appended to password when checking or changing the password. | Optional. #### Group table @@ -108,13 +108,13 @@ If you don't have any database model yet you can use below tables (MySQL): ``` CREATE TABLE sql_user ( - username VARCHAR(16) PRIMARY KEY, - display_name TEXT NULL, - email TEXT NULL, - home TEXT NULL, - password TEXT NOT NULL, - active TINYINT(1) NOT NULL DEFAULT '1', - can_change_avatar BOOLEAN NOT NULL DEFAULT FALSE + username VARCHAR(16) PRIMARY KEY, + display_name TEXT NULL, + email TEXT NULL, + home TEXT NULL, + password TEXT NOT NULL, + active TINYINT(1) NOT NULL DEFAULT '1', + provide_avatar BOOLEAN NOT NULL DEFAULT FALSE ); CREATE TABLE sql_group diff --git a/templates/admin.php b/templates/admin.php index 62e0d39..14d4683 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -148,7 +148,7 @@ function print_select_options( print_text_input($l, "db-table-user-column-password", "Password", $_['db.table.user.column.password']); print_text_input($l, "db-table-user-column-name", "Display name", $_['db.table.user.column.name']); print_text_input($l, "db-table-user-column-active", "Active", $_['db.table.user.column.active']); - print_text_input($l, "db-table-user-column-avatar", "Can change avatar", $_['db.table.user.column.avatar']); + print_text_input($l, "db-table-user-column-avatar", "Provide avatar", $_['db.table.user.column.avatar']); print_text_input($l, "db-table-user-column-salt", "Salt", $_['db.table.user.column.salt']); ?>
From ed37c7085d1750a863a67ba5e9faf74fe4f1eb0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Mon, 9 Jul 2018 19:21:53 +0200 Subject: [PATCH 27/30] User quota from SQL --- CHANGELOG.md | 2 + README.md | 5 +- js/settings.js | 2 +- lib/Action/EmailSync.php | 6 +- lib/Action/QuotaSync.php | 137 ++++++++++++++++++++++++++++++ lib/Backend/UserBackend.php | 9 ++ lib/Constant/App.php | 6 +- lib/Constant/DB.php | 1 + lib/Constant/Opt.php | 1 + lib/Constant/Query.php | 2 + lib/Model/User.php | 4 + lib/Query/QueryProvider.php | 8 +- lib/Repository/UserRepository.php | 2 + templates/admin.php | 2 + 14 files changed, 178 insertions(+), 9 deletions(-) create mode 100644 lib/Action/QuotaSync.php diff --git a/CHANGELOG.md b/CHANGELOG.md index b676f34..0ef9ce5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,11 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. - WoltLab Community Framework 2.x hash algorithm - phpass hash implementation - Support for salt column +- User quota synchronization ### Changed - Example SQL script in README file +- Fixed misspelling ### Fixed - Table and column autocomplete in settings panel diff --git a/README.md b/README.md index f2ea96f..92b63f3 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,8 @@ Name | Description | Details **Allow password change** | Can user change its password. The password change is propagated to the database. See [Hash algorithms](#hash-algorithms). | Optional.
Default: false. **Use cache** | Use database query results cache. The cache can be cleared any time with the *Clear cache* button click. | Optional.
Default: false. **Hash algorithm** | How users passwords are stored in the database. See [Hash algorithms](#hash-algorithms). | Mandatory. -**Email sync** | Sync e-mail address with the Nextcloud.
- *None* - Disables this feature. This is the default option.
- *Synchronise only once* - Copy the e-mail address to the Nextcloud storage if its not set.
- *Nextcloud always wins* - Always copy the e-mail address to the database. This updates the user table.
- *SQL always wins* - Always copy the e-mail address to the Nextcloud storage. | Optional.
Default: *None*.
Requires: user *Email* column. +**Email sync** | Sync e-mail address with the Nextcloud.
- *None* - Disables this feature. This is the default option.
- *Synchronise only once* - Copy the e-mail address to the Nextcloud preferences if its not set.
- *Nextcloud always wins* - Always copy the e-mail address to the database. This updates the user table.
- *SQL always wins* - Always copy the e-mail address to the Nextcloud preferences. | Optional.
Default: *None*.
Requires: user *Email* column. +**Quota sync** | Sync user quota with the Nextcloud.
- *None* - Disables this feature. This is the default option.
- *Synchronise only once* - Copy the user quota to the Nextcloud preferences if its not set.
- *Nextcloud always wins* - Always copy the user quota to the database. This updates the user table.
- *SQL always wins* - Always copy the user quota to the Nextcloud preferences. | Optional.
Default: *None*.
Requires: user *Quota* column. **Home mode** | User storage path.
- *Default* - Let the Nextcloud manage this. The default option.
- *Query* - Use location from the user table pointed by the *home* column.
- *Static* - Use static location. The `%u` variable is replaced with the username of the user. | Optional
Default: *Default*. **Home Location** | User storage path for the `static` *home mode*. | Mandatory if the *Home mode* is set to `Static`. @@ -64,6 +65,7 @@ Name | Description | Details **Table name** | The table name. | Mandatory for user backend. **Username** | Username column. | Mandatory for user backend. **Email** | E-mail column. | Mandatory for *Email sync* option. +**Quota** | Quota column. | Mandatory for *Quota sync* option. **Home** | Home path column. | Mandatory for `Query` *Home sync* option. **Password** | Password hash column. | Mandatory for user backend. **Display name** | Display name column. | Optional. @@ -111,6 +113,7 @@ CREATE TABLE sql_user username VARCHAR(16) PRIMARY KEY, display_name TEXT NULL, email TEXT NULL, + quota TEXT NULL, home TEXT NULL, password TEXT NOT NULL, active TINYINT(1) NOT NULL DEFAULT '1', diff --git a/js/settings.js b/js/settings.js index c42bb4a..0a42eaf 100644 --- a/js/settings.js +++ b/js/settings.js @@ -76,7 +76,7 @@ user_sql.adminSettingsUI = function () { ); autocomplete( - "#db-table-user-column-uid, #db-table-user-column-email, #db-table-user-column-home, #db-table-user-column-password, #db-table-user-column-name, #db-table-user-column-active, #db-table-user-column-avatar, #db-table-user-column-salt", + "#db-table-user-column-uid, #db-table-user-column-email, #db-table-user-column-quota, #db-table-user-column-home, #db-table-user-column-password, #db-table-user-column-name, #db-table-user-column-active, #db-table-user-column-avatar, #db-table-user-column-salt", "/apps/user_sql/settings/autocomplete/table/user" ); diff --git a/lib/Action/EmailSync.php b/lib/Action/EmailSync.php index 3c9bc79..cded1c2 100644 --- a/lib/Action/EmailSync.php +++ b/lib/Action/EmailSync.php @@ -94,7 +94,7 @@ class EmailSync implements IUserAction $result = false; switch ($this->properties[Opt::EMAIL_SYNC]) { - case App::EMAIL_INITIAL: + case App::SYNC_INITIAL: if (empty($ncMail) && !empty($user->email)) { $this->config->setUserValue( $user->uid, "settings", "email", $user->email @@ -103,7 +103,7 @@ class EmailSync implements IUserAction $result = true; break; - case App::EMAIL_FORCE_NC: + case App::SYNC_FORCE_NC: if (!empty($ncMail) && $user->email !== $ncMail) { $user = $this->userRepository->findByUid($user->uid); if (!($user instanceof User)) { @@ -115,7 +115,7 @@ class EmailSync implements IUserAction } break; - case App::EMAIL_FORCE_SQL: + case App::SYNC_FORCE_SQL: if (!empty($user->email) && $user->email !== $ncMail) { $this->config->setUserValue( $user->uid, "settings", "email", $user->email diff --git a/lib/Action/QuotaSync.php b/lib/Action/QuotaSync.php new file mode 100644 index 0000000..7f73c9b --- /dev/null +++ b/lib/Action/QuotaSync.php @@ -0,0 +1,137 @@ + + * @author Marcin Łojewski + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +namespace OCA\UserSQL\Action; + +use OCA\UserSQL\Constant\App; +use OCA\UserSQL\Constant\Opt; +use OCA\UserSQL\Model\User; +use OCA\UserSQL\Properties; +use OCA\UserSQL\Repository\UserRepository; +use OCP\IConfig; +use OCP\ILogger; + +/** + * Synchronizes the user quota. + * + * @author Marcin Łojewski + */ +class QuotaSync implements IUserAction +{ + /** + * @var string The application name. + */ + private $appName; + /** + * @var ILogger The logger instance. + */ + private $logger; + /** + * @var Properties The properties array. + */ + private $properties; + /** + * @var IConfig The config instance. + */ + private $config; + /** + * @var UserRepository The user repository. + */ + private $userRepository; + + /** + * The default constructor. + * + * @param string $appName The application name. + * @param ILogger $logger The logger instance. + * @param Properties $properties The properties array. + * @param IConfig $config The config instance. + * @param UserRepository $userRepository The user repository. + */ + public function __construct( + $appName, ILogger $logger, Properties $properties, IConfig $config, + UserRepository $userRepository + ) { + $this->appName = $appName; + $this->logger = $logger; + $this->properties = $properties; + $this->config = $config; + $this->userRepository = $userRepository; + } + + /** + * @inheritdoc + * @throws \OCP\PreConditionNotMetException + */ + public function doAction(User $user) + { + $this->logger->debug( + "Entering QuotaSync#doAction($user->uid)", ["app" => $this->appName] + ); + + $ncQuota = $this->config->getUserValue( + $user->uid, "files", "quota", "" + ); + + $result = false; + + switch ($this->properties[Opt::QUOTA_SYNC]) { + case App::SYNC_INITIAL: + if (empty($ncQuota) && !empty($user->quota)) { + $this->config->setUserValue( + $user->uid, "files", "quota", $user->quota + ); + } + + $result = true; + break; + case App::SYNC_FORCE_NC: + if (!empty($ncQuota) && $user->quota !== $ncQuota) { + $user = $this->userRepository->findByUid($user->uid); + if (!($user instanceof User)) { + break; + } + + $user->quota = $ncQuota; + $result = $this->userRepository->save($user); + } + + break; + case App::SYNC_FORCE_SQL: + if (!empty($user->quota) && $user->quota !== $ncQuota) { + $this->config->setUserValue( + $user->uid, "files", "quota", $user->quota + ); + } + + $result = true; + break; + } + + $this->logger->debug( + "Returning QuotaSync#doAction($user->uid): " . ($result ? "true" + : "false"), + ["app" => $this->appName] + ); + + return $result; + } +} diff --git a/lib/Backend/UserBackend.php b/lib/Backend/UserBackend.php index 6a8237b..442559e 100644 --- a/lib/Backend/UserBackend.php +++ b/lib/Backend/UserBackend.php @@ -24,6 +24,7 @@ namespace OCA\UserSQL\Backend; use OC\User\Backend; use OCA\UserSQL\Action\EmailSync; use OCA\UserSQL\Action\IUserAction; +use OCA\UserSQL\Action\QuotaSync; use OCA\UserSQL\Cache; use OCA\UserSQL\Constant\App; use OCA\UserSQL\Constant\DB; @@ -116,6 +117,14 @@ final class UserBackend extends Backend $this->userRepository ); } + if (!empty($this->properties[Opt::QUOTA_SYNC]) + && !empty($this->properties[DB::USER_QUOTA_COLUMN]) + ) { + $this->actions[] = new QuotaSync( + $this->appName, $this->logger, $this->properties, $this->config, + $this->userRepository + ); + } } /** diff --git a/lib/Constant/App.php b/lib/Constant/App.php index a2973ce..e0e6b63 100644 --- a/lib/Constant/App.php +++ b/lib/Constant/App.php @@ -34,7 +34,7 @@ final class App const HOME_QUERY = "query"; const HOME_STATIC = "static"; - const EMAIL_FORCE_NC = "force_nc"; - const EMAIL_FORCE_SQL = "force_sql"; - const EMAIL_INITIAL = "initial"; + const SYNC_FORCE_NC = "force_nc"; + const SYNC_FORCE_SQL = "force_sql"; + const SYNC_INITIAL = "initial"; } diff --git a/lib/Constant/DB.php b/lib/Constant/DB.php index ce0da21..b05fc0f 100644 --- a/lib/Constant/DB.php +++ b/lib/Constant/DB.php @@ -51,6 +51,7 @@ final class DB const USER_HOME_COLUMN = "db.table.user.column.home"; const USER_NAME_COLUMN = "db.table.user.column.name"; const USER_PASSWORD_COLUMN = "db.table.user.column.password"; + const USER_QUOTA_COLUMN = "db.table.user.column.quota"; const USER_SALT_COLUMN = "db.table.user.column.salt"; const USER_UID_COLUMN = "db.table.user.column.uid"; } diff --git a/lib/Constant/Opt.php b/lib/Constant/Opt.php index 56ce8b2..a1f6617 100644 --- a/lib/Constant/Opt.php +++ b/lib/Constant/Opt.php @@ -34,5 +34,6 @@ final class Opt const HOME_MODE = "opt.home_mode"; const NAME_CHANGE = "opt.name_change"; const PASSWORD_CHANGE = "opt.password_change"; + const QUOTA_SYNC = "opt.quota_sync"; const USE_CACHE = "opt.use_cache"; } diff --git a/lib/Constant/Query.php b/lib/Constant/Query.php index f67183c..86511e0 100644 --- a/lib/Constant/Query.php +++ b/lib/Constant/Query.php @@ -39,9 +39,11 @@ final class Query const FIND_USERS = "find_users"; const SAVE_USER = "save_user"; + const EMAIL_PARAM = "email"; const GID_PARAM = "gid"; const NAME_PARAM = "name"; const PASSWORD_PARAM = "password"; + const QUOTA_PARAM = "quota"; const SEARCH_PARAM = "search"; const UID_PARAM = "uid"; } diff --git a/lib/Model/User.php b/lib/Model/User.php index dcc9551..40ebf1c 100644 --- a/lib/Model/User.php +++ b/lib/Model/User.php @@ -36,6 +36,10 @@ class User * @var string The user's email address. */ public $email; + /** + * @var string The user quota. + */ + public $quota; /** * @var string The user's display name. */ diff --git a/lib/Query/QueryProvider.php b/lib/Query/QueryProvider.php index 49f2ac9..85a9f95 100644 --- a/lib/Query/QueryProvider.php +++ b/lib/Query/QueryProvider.php @@ -71,15 +71,18 @@ class QueryProvider implements \ArrayAccess $uHome = $this->properties[DB::USER_HOME_COLUMN]; $uName = $this->properties[DB::USER_NAME_COLUMN]; $uPassword = $this->properties[DB::USER_PASSWORD_COLUMN]; + $uQuota = $this->properties[DB::USER_QUOTA_COLUMN]; $uSalt = $this->properties[DB::USER_SALT_COLUMN]; $uUID = $this->properties[DB::USER_UID_COLUMN]; $ugGID = $this->properties[DB::USER_GROUP_GID_COLUMN]; $ugUID = $this->properties[DB::USER_GROUP_UID_COLUMN]; + $emailParam = Query::EMAIL_PARAM; $gidParam = Query::GID_PARAM; $nameParam = Query::NAME_PARAM; $passwordParam = Query::PASSWORD_PARAM; + $quotaParam = Query::QUOTA_PARAM; $searchParam = Query::SEARCH_PARAM; $uidParam = Query::UID_PARAM; @@ -91,6 +94,7 @@ class QueryProvider implements \ArrayAccess = "$uUID AS uid, " . (empty($uName) ? "null" : $uName) . " AS name, " . (empty($uEmail) ? "null" : $uEmail) . " AS email, " . + (empty($uQuota) ? "null" : $uQuota) . " AS quota, " . (empty($uHome) ? "null" : $uHome) . " AS home, " . (empty($uActive) ? "true" : $uActive) . " AS active, " . (empty($uAvatar) ? "false" : $uAvatar) . " AS avatar, " . @@ -156,7 +160,9 @@ class QueryProvider implements \ArrayAccess Query::SAVE_USER => "UPDATE $user " . "SET $uPassword = :$passwordParam, " . - "$uName = :$nameParam " . + "$uName = :$nameParam, " . + "$uEmail = :$emailParam, " . + "$uQuota = :$quotaParam " . "WHERE $uUID = :$uidParam", ]; } diff --git a/lib/Repository/UserRepository.php b/lib/Repository/UserRepository.php index 8f284b6..8ba593b 100644 --- a/lib/Repository/UserRepository.php +++ b/lib/Repository/UserRepository.php @@ -107,6 +107,8 @@ class UserRepository Query::SAVE_USER, [ Query::NAME_PARAM => $user->name, Query::PASSWORD_PARAM => $user->password, + Query::EMAIL_PARAM => $user->email, + Query::QUOTA_PARAM => $user->quota, Query::UID_PARAM => $user->uid ] ); diff --git a/templates/admin.php b/templates/admin.php index 14d4683..f64c847 100644 --- a/templates/admin.php +++ b/templates/admin.php @@ -131,6 +131,7 @@ function print_select_options( print_select_options($l, "opt-crypto_class", "Hash algorithm", $hashes, $_['opt.crypto_class']); print_select_options($l, "opt-email_sync", "Email sync", ["" => "None", "initial" => "Synchronise only once", "force_nc"=>"Nextcloud always wins", "force_sql"=>"SQL always wins"], $_['opt.email_sync']); + print_select_options($l, "opt-quota_sync", "Quota sync", ["" => "None", "initial" => "Synchronise only once", "force_nc"=>"Nextcloud always wins", "force_sql"=>"SQL always wins"], $_['opt.quota_sync']); print_select_options($l, "opt-home_mode", "Home mode", ["" => "Default", "query" => "Query", "static" => "Static"], $_['opt.home_mode']); print_text_input($l, "opt-home_location", "Home Location", $_['opt.home_location']); ?>
@@ -144,6 +145,7 @@ function print_select_options( Date: Mon, 9 Jul 2018 20:04:54 +0200 Subject: [PATCH 28/30] update screenshot --- img/screenshot.png | Bin 42650 -> 44404 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/img/screenshot.png b/img/screenshot.png index d47dbd87cf77cf016bd21ef857f90f8653ecd0b7..a8825c164800a235812ef138bb90e67f74e95b09 100644 GIT binary patch literal 44404 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 literal 42650 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 Y9yHwSuuo>1_&M!)OEhoI+3?5z1#-@V4*&oF From 8b1fb2306efd53370e5f99af1ad5395142c14e2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Sat, 11 Aug 2018 11:30:44 +0200 Subject: [PATCH 29/30] Better description for Home mode --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 92b63f3..faa18a3 100644 --- a/README.md +++ b/README.md @@ -53,8 +53,8 @@ Name | Description | Details **Hash algorithm** | How users passwords are stored in the database. See [Hash algorithms](#hash-algorithms). | Mandatory. **Email sync** | Sync e-mail address with the Nextcloud.
- *None* - Disables this feature. This is the default option.
- *Synchronise only once* - Copy the e-mail address to the Nextcloud preferences if its not set.
- *Nextcloud always wins* - Always copy the e-mail address to the database. This updates the user table.
- *SQL always wins* - Always copy the e-mail address to the Nextcloud preferences. | Optional.
Default: *None*.
Requires: user *Email* column. **Quota sync** | Sync user quota with the Nextcloud.
- *None* - Disables this feature. This is the default option.
- *Synchronise only once* - Copy the user quota to the Nextcloud preferences if its not set.
- *Nextcloud always wins* - Always copy the user quota to the database. This updates the user table.
- *SQL always wins* - Always copy the user quota to the Nextcloud preferences. | Optional.
Default: *None*.
Requires: user *Quota* column. -**Home mode** | User storage path.
- *Default* - Let the Nextcloud manage this. The default option.
- *Query* - Use location from the user table pointed by the *home* column.
- *Static* - Use static location. The `%u` variable is replaced with the username of the user. | Optional
Default: *Default*. -**Home Location** | User storage path for the `static` *home mode*. | Mandatory if the *Home mode* is set to `Static`. +**Home mode** | User storage path.
- *Default* - Let the Nextcloud manage this. The default option.
- *Query* - Use location from the user table pointed by the *home* column.
- *Static* - Use static location pointed by the *Home Location* option. | Optional
Default: *Default*. +**Home Location** | User storage path for the `Static` *Home mode*. The `%u` variable is replaced with the username of the user. | Mandatory if the *Home mode* is set to `Static`. #### User table From 4be294e8b711c45565c1666ec324b7c5f4f374e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Marcin=20=C5=81ojewski?= Date: Sat, 11 Aug 2018 11:45:52 +0200 Subject: [PATCH 30/30] Release of version 4.0.0 --- CHANGELOG.md | 4 ++-- appinfo/info.xml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f67f24..905a81b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). -## [Unreleased] +## [4.0.0] - 2018-08-11 ### Added - SHA512 Whirlpool hash algorithm - WoltLab Community Framework 2.x hash algorithm @@ -83,7 +83,7 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0. ### Changed - Supported version of ownCloud, Nextcloud: ownCloud 10, Nextcloud 12 -[Unreleased]: https://github.com/nextcloud/user_sql/compare/v4.0.0-rc2...develop +[4.0.0]: https://github.com/nextcloud/user_sql/compare/v4.0.0-rc2...v4.0.0 [4.0.0-rc2]: https://github.com/nextcloud/user_sql/compare/v4.0.0-rc1...v4.0.0-rc2 [4.0.0-rc1]: https://github.com/nextcloud/user_sql/compare/v3.1.0...v4.0.0-rc1 [3.1.0]: https://github.com/nextcloud/user_sql/compare/v2.4.0...v3.1.0 diff --git a/appinfo/info.xml b/appinfo/info.xml index 38207b0..83352bd 100644 --- a/appinfo/info.xml +++ b/appinfo/info.xml @@ -8,7 +8,7 @@ Retrieve the users and groups info. Allow the users to change their passwords. Sync the users' email addresses with the addresses stored by Nextcloud. - 4.0.0-dev + 4.0.0 agpl Marcin Łojewski Andreas Böhler