Code Coverage |
||||||||||
Lines |
Functions and Methods |
Classes and Traits |
||||||||
Total | |
81.16% |
56 / 69 |
|
50.00% |
3 / 6 |
CRAP | |
0.00% |
0 / 1 |
SwitchDbHashCommand | |
81.16% |
56 / 69 |
|
50.00% |
3 / 6 |
27.85 | |
0.00% |
0 / 1 |
__construct | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
1 | |||
configure | |
100.00% |
7 / 7 |
|
100.00% |
1 / 1 |
1 | |||
getConfigWriter | |
0.00% |
0 / 1 |
|
0.00% |
0 / 1 |
2 | |||
getOpenSsl | |
100.00% |
1 / 1 |
|
100.00% |
1 / 1 |
2 | |||
fixEntity | |
85.71% |
6 / 7 |
|
0.00% |
0 / 1 |
4.05 | |||
execute | |
78.85% |
41 / 52 |
|
0.00% |
0 / 1 |
17.13 |
1 | <?php |
2 | |
3 | /** |
4 | * Console command: switch database encryption algorithm. |
5 | * |
6 | * PHP version 8 |
7 | * |
8 | * Copyright (C) Villanova University 2020. |
9 | * |
10 | * This program is free software; you can redistribute it and/or modify |
11 | * it under the terms of the GNU General Public License version 2, |
12 | * as published by the Free Software Foundation. |
13 | * |
14 | * This program is distributed in the hope that it will be useful, |
15 | * but WITHOUT ANY WARRANTY; without even the implied warranty of |
16 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
17 | * GNU General Public License for more details. |
18 | * |
19 | * You should have received a copy of the GNU General Public License |
20 | * along with this program; if not, write to the Free Software |
21 | * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA |
22 | * |
23 | * @category VuFind |
24 | * @package Console |
25 | * @author Demian Katz <demian.katz@villanova.edu> |
26 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
27 | * @link https://vufind.org/wiki/development Wiki |
28 | */ |
29 | |
30 | namespace VuFindConsole\Command\Util; |
31 | |
32 | use Laminas\Config\Config; |
33 | use Laminas\Crypt\BlockCipher; |
34 | use Laminas\Crypt\Exception\InvalidArgumentException; |
35 | use Laminas\Crypt\Symmetric\Openssl; |
36 | use Symfony\Component\Console\Attribute\AsCommand; |
37 | use Symfony\Component\Console\Command\Command; |
38 | use Symfony\Component\Console\Input\InputArgument; |
39 | use Symfony\Component\Console\Input\InputInterface; |
40 | use Symfony\Component\Console\Output\OutputInterface; |
41 | use VuFind\Config\Locator as ConfigLocator; |
42 | use VuFind\Config\PathResolver; |
43 | use VuFind\Config\Writer as ConfigWriter; |
44 | use VuFind\Db\Entity\UserCardEntityInterface; |
45 | use VuFind\Db\Entity\UserEntityInterface; |
46 | use VuFind\Db\Service\DbServiceInterface; |
47 | use VuFind\Db\Service\UserCardServiceInterface; |
48 | use VuFind\Db\Service\UserServiceInterface; |
49 | |
50 | use function count; |
51 | |
52 | /** |
53 | * Console command: switch database encryption algorithm. |
54 | * |
55 | * @category VuFind |
56 | * @package Console |
57 | * @author Demian Katz <demian.katz@villanova.edu> |
58 | * @license http://opensource.org/licenses/gpl-2.0.php GNU General Public License |
59 | * @link https://vufind.org/wiki/development Wiki |
60 | */ |
61 | #[AsCommand( |
62 | name: 'util/switch_db_hash', |
63 | description: 'Encryption algorithm switcher' |
64 | )] |
65 | class SwitchDbHashCommand extends Command |
66 | { |
67 | /** |
68 | * Constructor |
69 | * |
70 | * @param Config $config VuFind configuration |
71 | * @param UserServiceInterface $userService User database service |
72 | * @param UserCardServiceInterface $userCardService UserCard database service |
73 | * @param ?string $name The name of the command; passing null means |
74 | * it must be set in configure() |
75 | * @param ?PathResolver $pathResolver Config file path resolver |
76 | */ |
77 | public function __construct( |
78 | protected Config $config, |
79 | protected UserServiceInterface $userService, |
80 | protected UserCardServiceInterface $userCardService, |
81 | ?string $name = null, |
82 | protected ?PathResolver $pathResolver = null |
83 | ) { |
84 | parent::__construct($name); |
85 | } |
86 | |
87 | /** |
88 | * Configure the command. |
89 | * |
90 | * @return void |
91 | */ |
92 | protected function configure() |
93 | { |
94 | $this |
95 | ->setHelp( |
96 | 'Switches the encryption algorithm in the database ' |
97 | . 'and config. Expects new algorithm and (optional) new key as' |
98 | . ' parameters.' |
99 | )->addArgument('newmethod', InputArgument::REQUIRED, 'Encryption method') |
100 | ->addArgument('newkey', InputArgument::OPTIONAL, 'Encryption key'); |
101 | } |
102 | |
103 | /** |
104 | * Get a config writer |
105 | * |
106 | * @param string $path Path of file to write |
107 | * |
108 | * @return ConfigWriter |
109 | */ |
110 | protected function getConfigWriter($path) |
111 | { |
112 | return new ConfigWriter($path); |
113 | } |
114 | |
115 | /** |
116 | * Get an OpenSsl object for the specified algorithm (or return null if the |
117 | * algorithm is 'none'). |
118 | * |
119 | * @param string $algorithm Encryption algorithm |
120 | * |
121 | * @return Openssl |
122 | */ |
123 | protected function getOpenSsl($algorithm) |
124 | { |
125 | return ($algorithm == 'none') ? null : new Openssl(compact('algorithm')); |
126 | } |
127 | |
128 | /** |
129 | * Re-encrypt an entity. |
130 | * |
131 | * @param AbstractDbService $service Database service |
132 | * @param UserEntityInterface|UserCardEntityInterface $entity Row to update |
133 | * @param ?BlockCipher $oldcipher Old cipher (null for none) |
134 | * @param BlockCipher $newcipher New cipher |
135 | * |
136 | * @return void |
137 | * @throws InvalidArgumentException |
138 | */ |
139 | protected function fixEntity( |
140 | DbServiceInterface $service, |
141 | UserEntityInterface|UserCardEntityInterface $entity, |
142 | ?BlockCipher $oldcipher, |
143 | BlockCipher $newcipher |
144 | ): void { |
145 | $oldEncrypted = $entity->getCatPassEnc(); |
146 | $pass = ($oldcipher && $oldEncrypted !== null) |
147 | ? $oldcipher->decrypt($oldEncrypted) |
148 | : $entity->getRawCatPassword(); |
149 | $entity->setRawCatPassword(null); |
150 | $entity->setCatPassEnc($pass === null ? null : $newcipher->encrypt($pass)); |
151 | $service->persistEntity($entity); |
152 | } |
153 | |
154 | /** |
155 | * Run the command. |
156 | * |
157 | * @param InputInterface $input Input object |
158 | * @param OutputInterface $output Output object |
159 | * |
160 | * @return int 0 for success |
161 | */ |
162 | protected function execute(InputInterface $input, OutputInterface $output) |
163 | { |
164 | // Validate command line arguments: |
165 | $newhash = $input->getArgument('newmethod'); |
166 | |
167 | // Pull existing encryption settings from the configuration: |
168 | if ( |
169 | !isset($this->config->Authentication->ils_encryption_key) |
170 | || !($this->config->Authentication->encrypt_ils_password ?? false) |
171 | ) { |
172 | $oldhash = 'none'; |
173 | $oldkey = null; |
174 | } else { |
175 | $oldhash = $this->config->Authentication->ils_encryption_algo |
176 | ?? 'blowfish'; |
177 | $oldkey = $this->config->Authentication->ils_encryption_key; |
178 | } |
179 | |
180 | // Pull new encryption settings from argument or config: |
181 | $newkey = $input->getArgument('newkey') ?? $oldkey; |
182 | |
183 | // No key specified AND no key on file = fatal error: |
184 | if ($newkey === null) { |
185 | $output->writeln('Please specify a key as the second parameter.'); |
186 | return 1; |
187 | } |
188 | |
189 | // If no changes were requested, abort early: |
190 | if ($oldkey == $newkey && $oldhash == $newhash) { |
191 | $output->writeln('No changes requested -- no action needed.'); |
192 | return 0; |
193 | } |
194 | |
195 | // Initialize Openssl first, so we can catch any illegal algorithms before |
196 | // making any changes: |
197 | try { |
198 | $oldCrypt = $this->getOpenSsl($oldhash); |
199 | $newCrypt = $this->getOpenSsl($newhash); |
200 | } catch (\Exception $e) { |
201 | $output->writeln($e->getMessage()); |
202 | return 1; |
203 | } |
204 | |
205 | // Next update the config file, so if we are unable to write the file, |
206 | // we don't go ahead and make unwanted changes to the database: |
207 | $configPath = $this->pathResolver |
208 | ? $this->pathResolver->getLocalConfigPath('config.ini', null, true) |
209 | : ConfigLocator::getLocalConfigPath('config.ini', null, true); |
210 | $output->writeln("\tUpdating $configPath..."); |
211 | $writer = $this->getConfigWriter($configPath); |
212 | $writer->set('Authentication', 'encrypt_ils_password', true); |
213 | $writer->set('Authentication', 'ils_encryption_algo', $newhash); |
214 | $writer->set('Authentication', 'ils_encryption_key', $newkey); |
215 | if (!$writer->save()) { |
216 | $output->writeln("\tWrite failed!"); |
217 | return 1; |
218 | } |
219 | |
220 | // Set up ciphers for use below: |
221 | if ($oldhash != 'none') { |
222 | $oldcipher = new BlockCipher($oldCrypt); |
223 | $oldcipher->setKey($oldkey); |
224 | } else { |
225 | $oldcipher = null; |
226 | } |
227 | $newcipher = new BlockCipher($newCrypt); |
228 | $newcipher->setKey($newkey); |
229 | |
230 | // Now do the database rewrite: |
231 | $users = $this->userService->getAllUsersWithCatUsernames(); |
232 | $cards = $this->userCardService->getAllRowsWithUsernames(); |
233 | $output->writeln("\tConverting hashes for " . count($users) . ' user(s).'); |
234 | foreach ($users as $row) { |
235 | try { |
236 | $this->fixEntity($this->userService, $row, $oldcipher, $newcipher); |
237 | } catch (\Exception $e) { |
238 | $output->writeln("Problem with user {$row->getUsername()}: " . (string)$e); |
239 | } |
240 | } |
241 | if (count($cards) > 0) { |
242 | $output->writeln("\tConverting hashes for " . count($cards) . ' card(s).'); |
243 | foreach ($cards as $entity) { |
244 | try { |
245 | $this->fixEntity($this->userCardService, $entity, $oldcipher, $newcipher); |
246 | } catch (\Exception $e) { |
247 | $output->writeln("Problem with card {$entity->getId()}: " . (string)$e); |
248 | } |
249 | } |
250 | } |
251 | |
252 | // If we got this far, all went well! |
253 | $output->writeln("\tFinished."); |
254 | return 0; |
255 | } |
256 | } |