Passed
Pull Request — master (#4920)
by Nils
06:05
created

MigrateUserHandlerTrait::migratePersonalItems()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 25
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
cc 6
eloc 14
c 1
b 0
f 1
nc 8
nop 1
dl 0
loc 25
rs 9.2222
1
<?php
2
/**
3
 * Teampass - a collaborative passwords manager.
4
 * ---
5
 * This file is part of the TeamPass project.
6
 * 
7
 * TeamPass is free software: you can redistribute it and/or modify it
8
 * under the terms of the GNU General Public License as published by
9
 * the Free Software Foundation, version 3 of the License.
10
 * 
11
 * TeamPass is distributed in the hope that it will be useful,
12
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
13
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14
 * GNU General Public License for more details.
15
 * 
16
 * You should have received a copy of the GNU General Public License
17
 * along with this program. If not, see <https://www.gnu.org/licenses/>.
18
 * 
19
 * Certain components of this file may be under different licenses. For
20
 * details, see the `licenses` directory or individual file headers.
21
 * ---
22
 * @file      MigrateUserHandlerTrait.php
23
 * @author    Nils Laumaillé ([email protected])
24
 * @copyright 2009-2025 Teampass.net
25
 * @license   GPL-3.0
26
 * @see       https://www.teampass.net
27
 */
28
29
use TeampassClasses\Language\Language;
30
31
trait MigrateUserHandlerTrait {
32
    abstract protected function completeTask();
33
34
    /**
35
     * Generate user keys
36
     * @param array $taskData Données de la tâche
37
     * @param array $arguments Arguments nécessaires pour la création des clés
38
     * @return void
39
     */
40
    private function migratePersonalItems($arguments) {
41
        // Get all subtasks related to this task
42
        $subtasks = DB::query(
43
            'SELECT * FROM ' . prefixTable('background_subtasks') . ' WHERE task_id = %i AND is_in_progress = 0 ORDER BY `task` ASC',
44
            $this->taskId
45
        );
46
    
47
        if (empty($subtasks)) {
48
            if (LOG_TASKS=== true) $this->logger->log("No subtask was found for task {$this->taskId}");
49
            return;
50
        }
51
    
52
        // Process each subtask
53
        foreach ($subtasks as $subtask) {
54
            if (LOG_TASKS=== true) $this->logger->log("Processing subtask {$subtask['increment_id']} for task {$this->taskId}");
55
            $this->processMigratePersonalItemsSubtask($subtask, $arguments);
56
        }
57
    
58
        // Are all subtasks completed?
59
        $remainingSubtasks = DB::queryFirstField(
60
            'SELECT COUNT(*) FROM ' . prefixTable('background_subtasks') . ' WHERE task_id = %i AND is_in_progress = 0',
61
            $this->taskId
62
        );    
63
        if ($remainingSubtasks == 0) {
64
            $this->completeTask();
65
        }
66
    }
67
    
68
69
    /**
70
     * Process a subtask for generating user keys.
71
     * @param array $subtask The subtask to process.
72
     * @param array $arguments Arguments for the task.
73
     * @return void
74
     */
75
    private function processMigratePersonalItemsSubtask(array $subtask, array $arguments) {
76
        try {
77
            $taskData = json_decode($subtask['task'], true);
78
            
79
            // Mark the subtask as in progress
80
            DB::update(
81
                prefixTable('background_subtasks'),
82
                [
83
                    'is_in_progress' => 1,
84
                    'updated_at' => time(),
85
                    'status' => 'in progress'
86
                ],
87
                'increment_id = %i',
88
                $subtask['increment_id']
89
            );
90
            
91
            if (LOG_TASKS=== true) $this->logger->log("Subtask is in progress: ".$taskData['step'], 'INFO');
92
            switch ($taskData['step'] ?? '') {
93
                case 'user-personal-items-migration-step10':
94
                    $this->migratePersonalItemsStep10($taskData, $arguments);
95
                    break;
96
                case 'user-personal-items-migration-step20':
97
                    $this->migratePersonalItemsStep20($taskData, $arguments);
98
                    break;
99
                case 'user-personal-items-migration-step30':
100
                    $this->migratePersonalItemsStep30($taskData, $arguments);
101
                    break;
102
                case 'user-personal-items-migration-step40':
103
                    $this->migratePersonalItemsStep40($taskData, $arguments);
104
                    break;
105
                case 'user-personal-items-migration-step50':
106
                    $this->migratePersonalItemsStep50($taskData, $arguments);
107
                    break;
108
                case 'user-personal-items-migration-step-final':
109
                    $this->migratePersonalItemsStepFinal($arguments);
110
                    break;
111
                break;
112
                default:
113
                    throw new Exception("Type of subtask unknown: {$this->processType}");
114
            }
115
    
116
            // Mark subtask as completed
117
            DB::update(
118
                prefixTable('background_subtasks'),
119
                [
120
                    'is_in_progress' => -1,
121
                    'finished_at' => time(),
122
                    'status' => 'completed',
123
                ],
124
                'increment_id = %i',
125
                $subtask['increment_id']
126
            );
127
    
128
        } catch (Exception $e) {
129
            // Failure handling
130
            DB::update(
131
                prefixTable('background_subtasks'),
132
                [
133
                    'is_in_progress' => -1,
134
                    'finished_at' => time(),
135
                    'updated_at' => time(),
136
                    'status' => 'failed',
137
                    'error_message' => $e->getMessage(),
138
                ],
139
                'increment_id = %i',
140
                $subtask['increment_id']
141
            );
142
            
143
            $this->logger->log("Subtask {$subtask['increment_id']} failure: " . $e->getMessage(), 'ERROR');
144
        }
145
    }
146
    
147
148
    /**
149
     * Generate new user keys
150
     * @param array $taskData Task data
151
     * @param array $arguments Arguments for the task
152
     */
153
    private function migratePersonalItemsStep10($taskData, $arguments): void
154
    {
155
        // get user private key
156
        $userInfo = $this->getOwnerInfos(
0 ignored issues
show
Bug introduced by
It seems like getOwnerInfos() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

156
        /** @scrutinizer ignore-call */ 
157
        $userInfo = $this->getOwnerInfos(
Loading history...
157
            $arguments['user_id'],
158
            $arguments['user_pwd'],
159
            1,
160
            $arguments['user_private_key'] ?? ''
161
        );
162
163
        // get TP_USER private key
164
        $userTP = DB::queryFirstRow(
165
            'SELECT pw, public_key, private_key
166
            FROM ' . prefixTable('users') . '
167
            WHERE id = %i',
168
            TP_USER_ID
169
        );
170
171
        // Start transaction for better performance
172
        DB::startTransaction();
173
174
        // Loop on items
175
        $rows = DB::query(
176
            'SELECT id, pw
177
            FROM ' . prefixTable('items') . '
178
            WHERE perso =  1
179
            AND id_tree IN %li
180
            ORDER BY id ASC
181
            LIMIT %i, %i',
182
            json_decode($arguments['personal_folders_ids']),
183
            $taskData['index'],
184
            $taskData['nb']
185
        );
186
        
187
        foreach ($rows as $item) {
188
            // Get itemKey from current user
189
            $shareKeyForItem = DB::queryFirstRow(
190
                'SELECT share_key, increment_id
191
                FROM ' . prefixTable('sharekeys_items') . '
192
                WHERE object_id = %i AND user_id = %i',
193
                $item['id'],
194
                (int) $arguments['user_id']
195
            );
196
197
            // do we have any input? (#3481)
198
            if ($shareKeyForItem === null || count($shareKeyForItem) === 0) {
199
                continue;
200
            }
201
202
            // Decrypt itemkey with user private key
203
            $itemKey = decryptUserObjectKey($shareKeyForItem['share_key'], $userInfo['private_key']);
204
            
205
            // Now create sharekey for user
206
            $this->createUserShareKey(
207
                'sharekeys_items',
208
                (string) $itemKey,
209
                (string) $userInfo['public_key'],
210
                (int) $item['id'],
211
                (int) $arguments['user_id']
212
            );
213
214
            // Now create sharekey for TP_USER
215
            $this->createUserShareKey(
216
                'sharekeys_items',
217
                (string) $itemKey,
218
                (string) $userTP['public_key'],
219
                (int) $item['id'],
220
                (int) TP_USER_ID
221
            );
222
        }
223
224
        // Commit transaction
225
        DB::commit();
226
    }
227
228
    /**
229
     * Create the sharekey for user
230
     * 
231
     * @param string $table Table name (sharekeys_items, sharekeys_fields, sharekeys_files, sharekeys_suggestions)
232
     * @param string $itemKey Item encryption key in hexadecimal format
233
     * @param string $publicKey User's RSA public key (PEM format)
234
     * @param int $recordId Record ID (object_id in sharekeys table)
235
     * @param int $userId User ID who will receive access to the encrypted key
236
     * @return void
237
     */
238
    private function createUserShareKey(string $table, string $itemKey, string $publicKey, int $recordId, int $userId): void
239
    {
240
        // Prevent to change key if its key is empty
241
        if (empty($itemKey) === true) {
242
            $share_key_for_item = '';
243
        } else {
244
            // Encrypt Item key
245
            $share_key_for_item = encryptUserObjectKey($itemKey, $publicKey);
246
        }
247
        
248
        // Save the new sharekey correctly encrypted in DB
249
        $affected = DB::update(
250
            prefixTable($table),
251
            array(
252
                'object_id' => $recordId,
253
                'user_id' => $userId,
254
                'share_key' => $share_key_for_item,
255
            ),
256
            'object_id = %i AND user_id = %i',
257
            $recordId,
258
            $userId
259
        );
260
        
261
        // If now row was updated, it means the user has no key for this item, so we need to insert it
262
        if ($affected === 0) {
263
            DB::insert(
264
                prefixTable($table),
265
                array(
266
                    'object_id' => $recordId,
267
                    'user_id' => $userId,
268
                    'share_key' => $share_key_for_item,
269
                )
270
            );
271
        }
272
    }
273
274
275
    /**
276
     * Generate new user keys - step 20
277
     * @param array $taskData Task data
278
     * @param array $arguments Arguments for the task
279
     * @return void
280
     */
281
    private function migratePersonalItemsStep20($taskData, $arguments) {
282
        // get user private key
283
        $userInfo = $this->getOwnerInfos(
284
            $arguments['user_id'],
285
            $arguments['user_pwd'],
286
            1,
287
            $arguments['user_private_key'] ?? ''
288
        );
289
290
        // get TP_USER private key
291
        $userTP = DB::queryFirstRow(
292
            'SELECT pw, public_key, private_key
293
            FROM ' . prefixTable('users') . '
294
            WHERE id = %i',
295
            TP_USER_ID
296
        );
297
298
        // Start transaction for better performance
299
        DB::startTransaction();
300
301
        // Loop on logs
302
        $rows = DB::query(
303
            'SELECT increment_id
304
            FROM ' . prefixTable('log_items') . '
305
            WHERE raison LIKE "at_pw :%" AND encryption_type = "teampass_aes"
306
            ORDER BY increment_id ASC
307
            LIMIT ' . $taskData['index'] . ', ' . $taskData['nb']
308
        );
309
        foreach ($rows as $record) {
310
            // Get itemKey from current user
311
            $currentUserKey = DB::queryFirstRow(
312
                'SELECT share_key
313
                FROM ' . prefixTable('sharekeys_logs') . '
314
                WHERE object_id = %i AND user_id = %i',
315
                $record['increment_id'],
316
                $arguments['user_id']
317
            );
318
319
            // do we have any input? (#3481)
320
            if ($currentUserKey === null || count($currentUserKey) === 0) {
321
                continue;
322
            }
323
324
            // Decrypt itemkey with admin key
325
            $itemKey = decryptUserObjectKey($currentUserKey['share_key'], $userInfo['private_key']);
326
            
327
            // Now create sharekey for user
328
            $this->createUserShareKey(
329
                'sharekeys_logs',
330
                (string) $itemKey,
331
                (string) $userInfo['public_key'],
332
                (int) $record['id'],
333
                (int) $arguments['user_id'],
334
            );
335
336
            // Now create sharekey for TP_USER
337
            $this->createUserShareKey(
338
                'sharekeys_logs',
339
                (string) $itemKey,
340
                (string) $userTP['public_key'],
341
                (int) $record['id'],
342
                (int) TP_USER_ID,
343
            );
344
        }
345
346
        // Commit transaction
347
        DB::commit();
348
    }
349
350
351
    /**
352
     * Generate new user keys - step 30
353
     * @param array $taskData Task data
354
     * @param array $arguments Arguments for the task
355
     * @return void
356
     */
357
    private function migratePersonalItemsStep30($taskData, $arguments) {
358
        // get user private key
359
        $userInfo = $this->getOwnerInfos(
360
            $arguments['user_id'],
361
            $arguments['user_pwd'],
362
            1,
363
            $arguments['user_private_key'] ?? ''
364
        );
365
366
        // get TP_USER private key
367
        $userTP = DB::queryFirstRow(
368
            'SELECT pw, public_key, private_key
369
            FROM ' . prefixTable('users') . '
370
            WHERE id = %i',
371
            TP_USER_ID
372
        );
373
374
        // Start transaction for better performance
375
        DB::startTransaction();
376
377
        // Loop on fields
378
        $rows = DB::query(
379
            'SELECT id
380
            FROM ' . prefixTable('categories_items') . '
381
            WHERE encryption_type = "teampass_aes"
382
            ORDER BY id ASC
383
            LIMIT %i, %i',
384
            $taskData['index'],
385
            $taskData['nb']
386
        );
387
        foreach ($rows as $record) {
388
            // Get itemKey from current user
389
            $currentUserKey = DB::queryFirstRow(
390
                'SELECT share_key
391
                FROM ' . prefixTable('sharekeys_fields') . '
392
                WHERE object_id = %i AND user_id = %i',
393
                $record['id'],
394
                $arguments['user_id']
395
            );
396
397
            if (isset($currentUserKey['share_key']) === true) {
398
                // Decrypt itemkey with user key
399
                $itemKey = decryptUserObjectKey($currentUserKey['share_key'], $userInfo['private_key']);
400
401
                // Now create sharekey for user
402
                $this->createUserShareKey(
403
                    'sharekeys_fields',
404
                    (string) $itemKey,
405
                    (string) $userInfo['public_key'],
406
                    (int) $record['id'],
407
                    (int) $arguments['user_id']
408
                );
409
410
                // Now create sharekey for TP_USER
411
                $this->createUserShareKey(
412
                    'sharekeys_fields',
413
                    (string) $itemKey,
414
                    (string) $userTP['public_key'],
415
                    (int) $record['id'],
416
                    (int) TP_USER_ID
417
                );
418
            }
419
        }
420
421
        // Commit transaction
422
        DB::commit();
423
    }
424
425
426
    /**
427
     * Generate new user keys - step 40
428
     * @param array $taskData Task data
429
     * @param array $arguments Arguments for the task
430
     * @return void
431
     */
432
    private function migratePersonalItemsStep40($taskData, $arguments) {
433
        // get user private key
434
        $userInfo = $this->getOwnerInfos(
435
            $arguments['user_id'],
436
            $arguments['user_pwd'],
437
            1,
438
            $arguments['user_private_key'] ?? ''
439
        );
440
441
        // get TP_USER private key
442
        $userTP = DB::queryFirstRow(
443
            'SELECT pw, public_key, private_key
444
            FROM ' . prefixTable('users') . '
445
            WHERE id = %i',
446
            TP_USER_ID
447
        );
448
449
        // Start transaction for better performance
450
        DB::startTransaction();
451
452
        // Loop on suggestions
453
        $rows = DB::query(
454
            'SELECT id
455
            FROM ' . prefixTable('suggestion') . '
456
            ORDER BY id ASC
457
            LIMIT %i, %i',
458
            $taskData['index'],
459
            $taskData['nb']
460
        );
461
        foreach ($rows as $record) {
462
            // Get itemKey from current user
463
            $currentUserKey = DB::queryFirstRow(
464
                'SELECT share_key
465
                FROM ' . prefixTable('sharekeys_suggestions') . '
466
                WHERE object_id = %i AND user_id = %i',
467
                $record['id'],
468
                $arguments['user_id']
469
            );
470
471
            // do we have any input? (#3481)
472
            if ($currentUserKey === null || count($currentUserKey) === 0) {
473
                continue;
474
            }
475
476
            // Decrypt itemkey with user key
477
            $itemKey = decryptUserObjectKey($currentUserKey['share_key'], $userInfo['private_key']);
478
479
            // Now create sharekey for user
480
            $this->createUserShareKey(
481
                'sharekeys_suggestions',
482
                (string) $itemKey,
483
                (string) $userInfo['public_key'],
484
                (int) $record['id'],
485
                (int) $arguments['user_id'],
486
            );
487
488
            // Now create sharekey for TP_USER
489
            $this->createUserShareKey(
490
                'sharekeys_fields',
491
                (string) $itemKey,
492
                (string) $userTP['public_key'],
493
                (int) $record['id'],
494
                (int) TP_USER_ID,
495
            );
496
        }
497
498
        // Commit transaction
499
        DB::commit();
500
    }
501
502
503
    /**
504
     * Generate new user keys - step 50
505
     * @param array $taskData Task data
506
     * @param array $arguments Arguments for the task
507
     * @return void
508
     */
509
    private function migratePersonalItemsStep50($taskData, $arguments) {
510
        // get user private key
511
        $userInfo = $this->getOwnerInfos(
512
            $arguments['user_id'],
513
            $arguments['user_pwd'],
514
            1,
515
            $arguments['user_private_key'] ?? ''
516
        );
517
518
        // get TP_USER private key
519
        $userTP = DB::queryFirstRow(
520
            'SELECT pw, public_key, private_key
521
            FROM ' . prefixTable('users') . '
522
            WHERE id = %i',
523
            TP_USER_ID
524
        );
525
526
        // Start transaction for better performance
527
        DB::startTransaction();
528
529
        // Loop on files
530
        $rows = DB::query(
531
            'SELECT f.id AS id, i.perso AS perso
532
            FROM ' . prefixTable('files') . ' AS f
533
            INNER JOIN ' . prefixTable('items') . ' AS i ON i.id = f.id_item
534
            WHERE f.status = "' . TP_ENCRYPTION_NAME . '"
535
            LIMIT %i, %i',
536
            $taskData['index'],
537
            $taskData['nb']
538
        ); //aes_encryption
539
        foreach ($rows as $record) {
540
            // Get itemKey from current user
541
            $currentUserKey = DB::queryFirstRow(
542
                'SELECT share_key, increment_id
543
                FROM ' . prefixTable('sharekeys_files') . '
544
                WHERE object_id = %i AND user_id = %i',
545
                $record['id'],
546
                (int) $arguments['user_id']
547
            );
548
549
            // do we have any input? (#3481)
550
            if ($currentUserKey === null || count($currentUserKey) === 0) {
551
                continue;
552
            }
553
554
            // Decrypt itemkey with user key
555
            $itemKey = decryptUserObjectKey($currentUserKey['share_key'], $userInfo['private_key']);
556
557
            // Now create sharekey for user
558
            $this->createUserShareKey(
559
                'sharekeys_suggestions',
560
                (string) $itemKey,
561
                (string) $userInfo['public_key'],
562
                (int) $record['id'],
563
                (int) $arguments['user_id'],
564
            );
565
566
            // Now create sharekey for TP_USER
567
            $this->createUserShareKey(
568
                'sharekeys_fields',
569
                (string) $itemKey,
570
                (string) $userTP['public_key'],
571
                (int) $record['id'],
572
                (int) TP_USER_ID,
573
            );
574
        }
575
576
        // Commit transaction
577
        DB::commit();
578
    }
579
580
581
    /**
582
     * Generate new user keys - step final
583
     * @param array $arguments Arguments for the task
584
     */
585
    private function migratePersonalItemsStepFinal($arguments) {
586
        $lang = new Language('english');
0 ignored issues
show
Unused Code introduced by
The assignment to $lang is dead and can be removed.
Loading history...
587
        
588
        // update LOG
589
        logEvents(
590
            $this->settings,
591
            'user_mngt',
592
            'at_user_new_keys',
593
            TP_USER_ID,
594
            "",
595
            (string) $arguments['user_id']
596
        );
597
598
        // Set user as ready for usage
599
        DB::update(
600
            prefixTable('users'),
601
            array(
602
                'ongoing_process_id' => NULL,
603
                'updated_at' => time(),
604
                'personal_items_migrated' => 1,
605
                'is_ready_for_usage' => 1,
606
            ),
607
            'id = %i',
608
            $arguments['user_id']
609
        );
610
    }    
611
}
612