keepassxc

Форк
0
/
Database.cpp 
1051 строка · 27.6 Кб
1
/*
2
 *  Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
3
 *  Copyright (C) 2010 Felix Geyer <debfx@fobos.de>
4
 *
5
 *  This program is free software: you can redistribute it and/or modify
6
 *  it under the terms of the GNU General Public License as published by
7
 *  the Free Software Foundation, either version 2 or (at your option)
8
 *  version 3 of the License.
9
 *
10
 *  This program is distributed in the hope that it will be useful,
11
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
12
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
13
 *  GNU General Public License for more details.
14
 *
15
 *  You should have received a copy of the GNU General Public License
16
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
17
 */
18

19
#include "Database.h"
20

21
#include "core/AsyncTask.h"
22
#include "core/FileWatcher.h"
23
#include "core/Group.h"
24
#include "crypto/Random.h"
25
#include "format/KdbxXmlReader.h"
26
#include "format/KeePass2Reader.h"
27
#include "format/KeePass2Writer.h"
28

29
#include <QFileInfo>
30
#include <QJsonObject>
31
#include <QRegularExpression>
32
#include <QSaveFile>
33
#include <QTemporaryFile>
34
#include <QTimer>
35

36
#ifdef Q_OS_WIN
37
#include <Windows.h>
38
#endif
39

40
QHash<QUuid, QPointer<Database>> Database::s_uuidMap;
41

42
Database::Database()
43
    : m_metadata(new Metadata(this))
44
    , m_data()
45
    , m_rootGroup(nullptr)
46
    , m_fileWatcher(new FileWatcher(this))
47
    , m_uuid(QUuid::createUuid())
48
{
49
    // setup modified timer
50
    m_modifiedTimer.setSingleShot(true);
51
    connect(this, &Database::emitModifiedChanged, this, [this](bool value) {
52
        if (!value) {
53
            stopModifiedTimer();
54
        }
55
    });
56
    connect(&m_modifiedTimer, &QTimer::timeout, this, &Database::emitModified);
57

58
    // other signals
59
    connect(m_metadata, &Metadata::modified, this, &Database::markAsModified);
60
    connect(this, &Database::databaseOpened, this, [this]() {
61
        updateCommonUsernames();
62
        updateTagList();
63
    });
64
    connect(this, &Database::modified, this, [this] { updateTagList(); });
65
    connect(this, &Database::databaseSaved, this, [this]() { updateCommonUsernames(); });
66
    connect(m_fileWatcher, &FileWatcher::fileChanged, this, &Database::databaseFileChanged);
67

68
    // static uuid map
69
    s_uuidMap.insert(m_uuid, this);
70

71
    // block modified signal and set root group
72
    setEmitModified(false);
73

74
    // Note: oldGroup is nullptr but need to respect return value capture
75
    auto oldGroup = setRootGroup(new Group());
76
    Q_UNUSED(oldGroup)
77

78
    m_modified = false;
79
    setEmitModified(true);
80
}
81

82
Database::Database(const QString& filePath)
83
    : Database()
84
{
85
    setFilePath(filePath);
86
}
87

88
Database::~Database()
89
{
90
    releaseData();
91
}
92

93
QUuid Database::uuid() const
94
{
95
    return m_uuid;
96
}
97

98
/**
99
 * Open the database from a previously specified file.
100
 * Unless `readOnly` is set to false, the database will be opened in
101
 * read-write mode and fall back to read-only if that is not possible.
102
 *
103
 * @param key composite key for unlocking the database
104
 * @param error error message in case of failure
105
 * @return true on success
106
 */
107
bool Database::open(QSharedPointer<const CompositeKey> key, QString* error)
108
{
109
    Q_ASSERT(!m_data.filePath.isEmpty());
110
    if (m_data.filePath.isEmpty()) {
111
        return false;
112
    }
113
    return open(m_data.filePath, std::move(key), error);
114
}
115

116
/**
117
 * Open the database from a file.
118
 * Unless `readOnly` is set to false, the database will be opened in
119
 * read-write mode and fall back to read-only if that is not possible.
120
 *
121
 * If key is provided as null, only headers will be read.
122
 *
123
 * @param filePath path to the file
124
 * @param key composite key for unlocking the database
125
 * @param error error message in case of failure
126
 * @return true on success
127
 */
128
bool Database::open(const QString& filePath, QSharedPointer<const CompositeKey> key, QString* error)
129
{
130
    QFile dbFile(filePath);
131
    if (!dbFile.exists()) {
132
        if (error) {
133
            *error = tr("File %1 does not exist.").arg(filePath);
134
        }
135
        return false;
136
    }
137

138
    // Don't autodetect read-only mode, as it triggers an upstream bug.
139
    // See https://github.com/keepassxreboot/keepassxc/issues/803
140
    // if (!readOnly && !dbFile.open(QIODevice::ReadWrite)) {
141
    //     readOnly = true;
142
    // }
143
    //
144
    // if (!dbFile.isOpen() && !dbFile.open(QIODevice::ReadOnly)) {
145
    if (!dbFile.open(QIODevice::ReadOnly)) {
146
        if (error) {
147
            *error = tr("Unable to open file %1.").arg(filePath);
148
        }
149
        return false;
150
    }
151

152
    setEmitModified(false);
153

154
    KeePass2Reader reader;
155
    if (!reader.readDatabase(&dbFile, std::move(key), this)) {
156
        if (error) {
157
            *error = tr("Error while reading the database: %1").arg(reader.errorString());
158
        }
159
        return false;
160
    }
161

162
    setFilePath(filePath);
163
    dbFile.close();
164

165
    markAsClean();
166

167
    emit databaseOpened();
168
    m_fileWatcher->start(canonicalFilePath(), 30, 1);
169
    setEmitModified(true);
170

171
    return true;
172
}
173

174
/**
175
 * KDBX format version.
176
 */
177
quint32 Database::formatVersion() const
178
{
179
    return m_data.formatVersion;
180
}
181

182
void Database::setFormatVersion(quint32 version)
183
{
184
    m_data.formatVersion = version;
185
}
186

187
/**
188
 * Whether the KDBX minor version is greater than the newest supported.
189
 */
190
bool Database::hasMinorVersionMismatch() const
191
{
192
    return m_data.formatVersion > KeePass2::FILE_VERSION_MAX;
193
}
194

195
bool Database::isSaving()
196
{
197
    bool locked = m_saveMutex.tryLock();
198
    if (locked) {
199
        m_saveMutex.unlock();
200
    }
201
    return !locked;
202
}
203

204
/**
205
 * Save the database to the current file path. It is an error to call this function
206
 * if no file path has been defined.
207
 *
208
 * @param error error message in case of failure
209
 * @param atomic Use atomic file transactions
210
 * @param backupFilePath Absolute file path to write the backup file to. Pass an empty QString to disable backup.
211
 * @return true on success
212
 */
213
bool Database::save(SaveAction action, const QString& backupFilePath, QString* error)
214
{
215
    Q_ASSERT(!m_data.filePath.isEmpty());
216
    if (m_data.filePath.isEmpty()) {
217
        if (error) {
218
            *error = tr("Could not save, database does not point to a valid file.");
219
        }
220
        return false;
221
    }
222

223
    return saveAs(m_data.filePath, action, backupFilePath, error);
224
}
225

226
/**
227
 * Save the database to a specific file.
228
 *
229
 * If atomic is false, this function uses QTemporaryFile instead of QSaveFile
230
 * due to a bug in Qt (https://bugreports.qt.io/browse/QTBUG-57299) that may
231
 * prevent the QSaveFile from renaming itself when using Dropbox, Google Drive,
232
 * or OneDrive.
233
 *
234
 * The risk in using QTemporaryFile is that the rename function is not atomic
235
 * and may result in loss of data if there is a crash or power loss at the
236
 * wrong moment.
237
 *
238
 * @param filePath Absolute path of the file to save
239
 * @param error error message in case of failure
240
 * @param atomic Use atomic file transactions
241
 * @param backupFilePath Absolute path to the location where the backup should be stored. Passing an empty string
242
 * disables backup.
243
 * @return true on success
244
 */
245
bool Database::saveAs(const QString& filePath, SaveAction action, const QString& backupFilePath, QString* error)
246
{
247
    // Disallow overlapping save operations
248
    if (isSaving()) {
249
        if (error) {
250
            *error = tr("Database save is already in progress.");
251
        }
252
        return false;
253
    }
254

255
    // Never save an uninitialized database
256
    if (!isInitialized()) {
257
        if (error) {
258
            *error = tr("Could not save, database has not been initialized!");
259
        }
260
        return false;
261
    }
262

263
    if (filePath == m_data.filePath) {
264
        // Fail-safe check to make sure we don't overwrite underlying file changes
265
        // that have not yet triggered a file reload/merge operation.
266
        if (!m_fileWatcher->hasSameFileChecksum()) {
267
            if (error) {
268
                *error = tr("Database file has unmerged changes.");
269
            }
270
            return false;
271
        }
272
    }
273

274
    // Clear read-only flag
275
    m_fileWatcher->stop();
276

277
    // Add random data to prevent side-channel data deduplication attacks
278
    int length = Random::instance()->randomUIntRange(64, 512);
279
    m_metadata->customData()->set(CustomData::RandomSlug, Random::instance()->randomArray(length).toHex());
280

281
    // Prevent destructive operations while saving
282
    QMutexLocker locker(&m_saveMutex);
283

284
    QFileInfo fileInfo(filePath);
285
    auto realFilePath = fileInfo.exists() ? fileInfo.canonicalFilePath() : fileInfo.absoluteFilePath();
286
    bool isNewFile = !QFile::exists(realFilePath);
287

288
#ifdef Q_OS_WIN
289
    bool isHidden = fileInfo.isHidden();
290
#endif
291

292
    bool ok = AsyncTask::runAndWaitForFuture([&] { return performSave(realFilePath, action, backupFilePath, error); });
293
    if (ok) {
294
        setFilePath(filePath);
295
        markAsClean();
296
        if (isNewFile) {
297
            QFile::setPermissions(realFilePath, QFile::ReadUser | QFile::WriteUser);
298
        }
299

300
#ifdef Q_OS_WIN
301
        if (isHidden) {
302
            SetFileAttributes(realFilePath.toStdString().c_str(), FILE_ATTRIBUTE_HIDDEN);
303
        }
304
#endif
305

306
        m_fileWatcher->start(realFilePath, 30, 1);
307
    } else {
308
        // Saving failed, don't rewatch file since it does not represent our database
309
        markAsModified();
310
    }
311

312
    return ok;
313
}
314

315
bool Database::performSave(const QString& filePath, SaveAction action, const QString& backupFilePath, QString* error)
316
{
317
    if (!backupFilePath.isNull()) {
318
        backupDatabase(filePath, backupFilePath);
319
    }
320

321
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
322
    QFileInfo info(filePath);
323
    auto createTime = info.exists() ? info.birthTime() : QDateTime::currentDateTime();
324
#endif
325

326
    switch (action) {
327
    case Atomic: {
328
        QSaveFile saveFile(filePath);
329
        if (saveFile.open(QIODevice::WriteOnly)) {
330
            // write the database to the file
331
            if (!writeDatabase(&saveFile, error)) {
332
                return false;
333
            }
334

335
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
336
            // Retain original creation time
337
            saveFile.setFileTime(createTime, QFile::FileBirthTime);
338
#endif
339

340
            if (saveFile.commit()) {
341
                // successfully saved database file
342
                return true;
343
            }
344
        }
345

346
        if (error) {
347
            *error = saveFile.errorString();
348
        }
349
        break;
350
    }
351
    case TempFile: {
352
        QTemporaryFile tempFile;
353
        if (tempFile.open()) {
354
            // write the database to the file
355
            if (!writeDatabase(&tempFile, error)) {
356
                return false;
357
            }
358
            tempFile.close(); // flush to disk
359

360
            // Delete the original db and move the temp file in place
361
            auto perms = QFile::permissions(filePath);
362
            QFile::remove(filePath);
363

364
            // Note: call into the QFile rename instead of QTemporaryFile
365
            // due to an undocumented difference in how the function handles
366
            // errors. This prevents errors when saving across file systems.
367
            if (tempFile.QFile::rename(filePath)) {
368
                // successfully saved the database
369
                tempFile.setAutoRemove(false);
370
                QFile::setPermissions(filePath, perms);
371
#if QT_VERSION >= QT_VERSION_CHECK(5, 10, 0)
372
                // Retain original creation time
373
                tempFile.setFileTime(createTime, QFile::FileBirthTime);
374
#endif
375
                return true;
376
            } else if (backupFilePath.isEmpty() || !restoreDatabase(filePath, backupFilePath)) {
377
                // Failed to copy new database in place, and
378
                // failed to restore from backup or backups disabled
379
                tempFile.setAutoRemove(false);
380
                if (error) {
381
                    *error = tr("%1\nBackup database located at %2").arg(tempFile.errorString(), tempFile.fileName());
382
                }
383
                return false;
384
            }
385
        }
386

387
        if (error) {
388
            *error = tempFile.errorString();
389
        }
390
        break;
391
    }
392
    case DirectWrite: {
393
        // Open the original database file for direct-write
394
        QFile dbFile(filePath);
395
        if (dbFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
396
            if (!writeDatabase(&dbFile, error)) {
397
                return false;
398
            }
399
            dbFile.close();
400
            return true;
401
        }
402
        if (error) {
403
            *error = dbFile.errorString();
404
        }
405
        break;
406
    }
407
    }
408

409
    // Saving failed
410
    return false;
411
}
412

413
bool Database::writeDatabase(QIODevice* device, QString* error)
414
{
415
    Q_ASSERT(m_data.key);
416
    Q_ASSERT(m_data.transformedDatabaseKey);
417

418
    PasswordKey oldTransformedKey;
419
    if (m_data.key->isEmpty()) {
420
        oldTransformedKey.setRawKey(m_data.transformedDatabaseKey->rawKey());
421
    }
422

423
    KeePass2Writer writer;
424
    setEmitModified(false);
425
    writer.writeDatabase(device, this);
426
    setEmitModified(true);
427

428
    if (writer.hasError()) {
429
        if (error) {
430
            *error = writer.errorString();
431
        }
432
        return false;
433
    }
434

435
    QByteArray newKey = m_data.transformedDatabaseKey->rawKey();
436
    Q_ASSERT(!newKey.isEmpty());
437
    Q_ASSERT(newKey != oldTransformedKey.rawKey());
438
    if (newKey.isEmpty() || newKey == oldTransformedKey.rawKey()) {
439
        if (error) {
440
            *error = tr("Key not transformed. This is a bug, please report it to the developers.");
441
        }
442
        return false;
443
    }
444

445
    return true;
446
}
447

448
bool Database::extract(QByteArray& xmlOutput, QString* error)
449
{
450
    KeePass2Writer writer;
451
    writer.extractDatabase(this, xmlOutput);
452
    if (writer.hasError()) {
453
        if (error) {
454
            *error = writer.errorString();
455
        }
456
        return false;
457
    }
458

459
    return true;
460
}
461

462
bool Database::import(const QString& xmlExportPath, QString* error)
463
{
464
    KdbxXmlReader reader(KeePass2::FILE_VERSION_4);
465
    QFile file(xmlExportPath);
466
    file.open(QIODevice::ReadOnly);
467

468
    reader.readDatabase(&file, this);
469

470
    if (reader.hasError()) {
471
        if (error) {
472
            *error = reader.errorString();
473
        }
474
        return false;
475
    }
476

477
    return true;
478
}
479

480
/**
481
 * Release all stored group, entry, and meta data of this database.
482
 *
483
 * Call this method to ensure all data is cleared even if valid
484
 * pointers to this Database object are still being held.
485
 *
486
 * A previously reparented root group will not be freed.
487
 */
488

489
void Database::releaseData()
490
{
491
    // Prevent data release while saving
492
    Q_ASSERT(!isSaving());
493
    QMutexLocker locker(&m_saveMutex);
494

495
    if (m_modified) {
496
        emit databaseDiscarded();
497
    }
498

499
    setEmitModified(false);
500
    m_modified = false;
501

502
    s_uuidMap.remove(m_uuid);
503
    m_uuid = QUuid();
504

505
    m_data.clear();
506
    m_metadata->clear();
507

508
    // Reset and delete the root group
509
    auto oldGroup = setRootGroup(new Group());
510
    delete oldGroup;
511

512
    m_fileWatcher->stop();
513

514
    m_deletedObjects.clear();
515
    m_commonUsernames.clear();
516
    m_tagList.clear();
517
}
518

519
/**
520
 * Remove the old backup and replace it with a new one. Backup name is taken from destinationFilePath.
521
 * Non-existing parent directories will be created automatically.
522
 *
523
 * @param filePath Path to the file to backup
524
 * @param destinationFilePath Path to the backup destination file
525
 * @return true on success
526
 */
527
bool Database::backupDatabase(const QString& filePath, const QString& destinationFilePath)
528
{
529
    // Ensure that the path to write to actually exists
530
    auto parentDirectory = QFileInfo(destinationFilePath).absoluteDir();
531
    if (!parentDirectory.exists()) {
532
        if (!QDir().mkpath(parentDirectory.absolutePath())) {
533
            return false;
534
        }
535
    }
536
    auto perms = QFile::permissions(filePath);
537
    QFile::remove(destinationFilePath);
538
    bool res = QFile::copy(filePath, destinationFilePath);
539
    QFile::setPermissions(destinationFilePath, perms);
540
    return res;
541
}
542

543
/**
544
 * Restores the database file from the backup file with
545
 * name <filename>.old.<extension> to filePath. This will
546
 * overwrite the existing file!
547
 *
548
 * @param filePath Path to the file to restore
549
 * @return true on success
550
 */
551
bool Database::restoreDatabase(const QString& filePath, const QString& fromBackupFilePath)
552
{
553
    auto perms = QFile::permissions(filePath);
554
    // Only try to restore if the backup file actually exists
555
    if (QFile::exists(fromBackupFilePath)) {
556
        QFile::remove(filePath);
557
        if (QFile::copy(fromBackupFilePath, filePath)) {
558
            return QFile::setPermissions(filePath, perms);
559
        }
560
    }
561
    return false;
562
}
563

564
/**
565
 * Returns true if the database key exists, has subkeys, and the
566
 * root group exists
567
 *
568
 * @return true if database has been fully initialized
569
 */
570
bool Database::isInitialized() const
571
{
572
    return m_data.key && !m_data.key->isEmpty() && m_rootGroup;
573
}
574

575
Group* Database::rootGroup()
576
{
577
    return m_rootGroup;
578
}
579

580
const Group* Database::rootGroup() const
581
{
582
    return m_rootGroup;
583
}
584

585
/* Set the root group of the database and return
586
 * the old root group. It is the responsibility
587
 * of the calling function to dispose of the old
588
 * root group.
589
 */
590
Group* Database::setRootGroup(Group* group)
591
{
592
    Q_ASSERT(group);
593

594
    if (isInitialized() && isModified()) {
595
        emit databaseDiscarded();
596
    }
597

598
    auto oldRoot = m_rootGroup;
599
    m_rootGroup = group;
600
    m_rootGroup->setParent(this);
601

602
    // Initialize the root group if not done already
603
    if (m_rootGroup->uuid().isNull()) {
604
        m_rootGroup->setUuid(QUuid::createUuid());
605
        m_rootGroup->setName(tr("Passwords", "Root group name"));
606
    }
607

608
    return oldRoot;
609
}
610

611
Metadata* Database::metadata()
612
{
613
    return m_metadata;
614
}
615

616
const Metadata* Database::metadata() const
617
{
618
    return m_metadata;
619
}
620

621
/**
622
 * Returns the original file path that was provided for
623
 * this database. This path may not exist, may contain
624
 * unresolved symlinks, or have malformed slashes.
625
 *
626
 * @return original file path
627
 */
628
QString Database::filePath() const
629
{
630
    return m_data.filePath;
631
}
632

633
/**
634
 * Returns the canonical file path of this databases'
635
 * set file path. This returns an empty string if the
636
 * file does not exist or cannot be resolved.
637
 *
638
 * @return canonical file path
639
 */
640
QString Database::canonicalFilePath() const
641
{
642
    QFileInfo fileInfo(m_data.filePath);
643
    return fileInfo.canonicalFilePath();
644
}
645

646
void Database::setFilePath(const QString& filePath)
647
{
648
    if (filePath != m_data.filePath) {
649
        QString oldPath = m_data.filePath;
650
        m_data.filePath = filePath;
651
        // Don't watch for changes until the next open or save operation
652
        m_fileWatcher->stop();
653
        emit filePathChanged(oldPath, filePath);
654
    }
655
}
656

657
QList<DeletedObject> Database::deletedObjects()
658
{
659
    return m_deletedObjects;
660
}
661

662
const QList<DeletedObject>& Database::deletedObjects() const
663
{
664
    return m_deletedObjects;
665
}
666

667
bool Database::containsDeletedObject(const QUuid& uuid) const
668
{
669
    for (const DeletedObject& currentObject : m_deletedObjects) {
670
        if (currentObject.uuid == uuid) {
671
            return true;
672
        }
673
    }
674
    return false;
675
}
676

677
bool Database::containsDeletedObject(const DeletedObject& object) const
678
{
679
    for (const DeletedObject& currentObject : m_deletedObjects) {
680
        if (currentObject.uuid == object.uuid) {
681
            return true;
682
        }
683
    }
684
    return false;
685
}
686

687
void Database::setDeletedObjects(const QList<DeletedObject>& delObjs)
688
{
689
    if (m_deletedObjects == delObjs) {
690
        return;
691
    }
692
    m_deletedObjects = delObjs;
693
}
694

695
void Database::addDeletedObject(const DeletedObject& delObj)
696
{
697
    Q_ASSERT(delObj.deletionTime.timeSpec() == Qt::UTC);
698
    m_deletedObjects.append(delObj);
699
}
700

701
void Database::addDeletedObject(const QUuid& uuid)
702
{
703
    DeletedObject delObj;
704
    delObj.deletionTime = Clock::currentDateTimeUtc();
705
    delObj.uuid = uuid;
706

707
    addDeletedObject(delObj);
708
}
709

710
const QStringList& Database::commonUsernames() const
711
{
712
    return m_commonUsernames;
713
}
714

715
const QStringList& Database::tagList() const
716
{
717
    return m_tagList;
718
}
719

720
void Database::updateCommonUsernames(int topN)
721
{
722
    m_commonUsernames.clear();
723
    m_commonUsernames.append(rootGroup()->usernamesRecursive(topN));
724
}
725

726
void Database::updateTagList()
727
{
728
    m_tagList.clear();
729
    if (!m_rootGroup) {
730
        emit tagListUpdated();
731
        return;
732
    }
733

734
    // Search groups recursively looking for tags
735
    // Use a set to prevent adding duplicates
736
    QSet<QString> tagSet;
737
    for (auto entry : m_rootGroup->entriesRecursive()) {
738
        if (!entry->isRecycled()) {
739
            for (auto tag : entry->tagList()) {
740
                tagSet.insert(tag);
741
            }
742
        }
743
    }
744

745
    m_tagList = tagSet.toList();
746
    m_tagList.sort();
747
    emit tagListUpdated();
748
}
749

750
void Database::removeTag(const QString& tag)
751
{
752
    if (!m_rootGroup) {
753
        return;
754
    }
755

756
    for (auto entry : m_rootGroup->entriesRecursive()) {
757
        entry->removeTag(tag);
758
    }
759
}
760

761
const QUuid& Database::cipher() const
762
{
763
    return m_data.cipher;
764
}
765

766
Database::CompressionAlgorithm Database::compressionAlgorithm() const
767
{
768
    return m_data.compressionAlgorithm;
769
}
770

771
QByteArray Database::transformedDatabaseKey() const
772
{
773
    Q_ASSERT(m_data.transformedDatabaseKey);
774
    if (!m_data.transformedDatabaseKey) {
775
        return {};
776
    }
777
    return m_data.transformedDatabaseKey->rawKey();
778
}
779

780
QByteArray Database::challengeResponseKey() const
781
{
782
    Q_ASSERT(m_data.challengeResponseKey);
783
    if (!m_data.challengeResponseKey) {
784
        return {};
785
    }
786
    return m_data.challengeResponseKey->rawKey();
787
}
788

789
bool Database::challengeMasterSeed(const QByteArray& masterSeed)
790
{
791
    Q_ASSERT(m_data.key);
792
    Q_ASSERT(m_data.masterSeed);
793

794
    m_keyError.clear();
795
    if (m_data.key && m_data.masterSeed) {
796
        m_data.masterSeed->setRawKey(masterSeed);
797
        QByteArray response;
798
        bool ok = m_data.key->challenge(masterSeed, response, &m_keyError);
799
        if (ok && !response.isEmpty()) {
800
            m_data.challengeResponseKey->setRawKey(response);
801
        } else if (ok && response.isEmpty()) {
802
            // no CR key present, make sure buffer is empty
803
            m_data.challengeResponseKey.reset(new PasswordKey);
804
        }
805
        return ok;
806
    }
807
    return false;
808
}
809

810
void Database::setCipher(const QUuid& cipher)
811
{
812
    Q_ASSERT(!cipher.isNull());
813

814
    m_data.cipher = cipher;
815
}
816

817
void Database::setCompressionAlgorithm(Database::CompressionAlgorithm algo)
818
{
819
    Q_ASSERT(static_cast<quint32>(algo) <= CompressionAlgorithmMax);
820

821
    m_data.compressionAlgorithm = algo;
822
}
823

824
/**
825
 * Set and transform a new encryption key.
826
 *
827
 * @param key key to set and transform or nullptr to reset the key
828
 * @param updateChangedTime true to update database change time
829
 * @param updateTransformSalt true to update the transform salt
830
 * @param transformKey trigger the KDF after setting the key
831
 * @return true on success
832
 */
833
bool Database::setKey(const QSharedPointer<const CompositeKey>& key,
834
                      bool updateChangedTime,
835
                      bool updateTransformSalt,
836
                      bool transformKey)
837
{
838
    m_keyError.clear();
839

840
    if (!key) {
841
        m_data.resetKeys();
842
        return true;
843
    }
844

845
    if (updateTransformSalt) {
846
        m_data.kdf->randomizeSeed();
847
        Q_ASSERT(!m_data.kdf->seed().isEmpty());
848
    }
849

850
    PasswordKey oldTransformedDatabaseKey;
851
    if (m_data.key && !m_data.key->isEmpty()) {
852
        oldTransformedDatabaseKey.setRawKey(m_data.transformedDatabaseKey->rawKey());
853
    }
854

855
    QByteArray transformedDatabaseKey;
856

857
    if (!transformKey) {
858
        transformedDatabaseKey = QByteArray(oldTransformedDatabaseKey.rawKey());
859
    } else if (!key->transform(*m_data.kdf, transformedDatabaseKey, &m_keyError)) {
860
        return false;
861
    }
862

863
    m_data.key = key;
864
    if (!transformedDatabaseKey.isEmpty()) {
865
        m_data.transformedDatabaseKey->setRawKey(transformedDatabaseKey);
866
    }
867
    if (updateChangedTime) {
868
        m_metadata->setDatabaseKeyChanged(Clock::currentDateTimeUtc());
869
    }
870

871
    if (oldTransformedDatabaseKey.rawKey() != m_data.transformedDatabaseKey->rawKey()) {
872
        markAsModified();
873
    }
874

875
    return true;
876
}
877

878
QString Database::keyError()
879
{
880
    return m_keyError;
881
}
882

883
QVariantMap& Database::publicCustomData()
884
{
885
    return m_data.publicCustomData;
886
}
887

888
const QVariantMap& Database::publicCustomData() const
889
{
890
    return m_data.publicCustomData;
891
}
892

893
void Database::setPublicCustomData(const QVariantMap& customData)
894
{
895
    m_data.publicCustomData = customData;
896
}
897

898
void Database::createRecycleBin()
899
{
900
    auto recycleBin = new Group();
901
    recycleBin->setUuid(QUuid::createUuid());
902
    recycleBin->setParent(rootGroup());
903
    recycleBin->setName(tr("Recycle Bin"));
904
    recycleBin->setIcon(Group::RecycleBinIconNumber);
905
    recycleBin->setSearchingEnabled(Group::Disable);
906
    recycleBin->setAutoTypeEnabled(Group::Disable);
907

908
    m_metadata->setRecycleBin(recycleBin);
909
}
910

911
void Database::recycleEntry(Entry* entry)
912
{
913
    if (m_metadata->recycleBinEnabled()) {
914
        if (!m_metadata->recycleBin()) {
915
            createRecycleBin();
916
        }
917
        entry->setGroup(metadata()->recycleBin());
918
    } else {
919
        delete entry;
920
    }
921
}
922

923
void Database::recycleGroup(Group* group)
924
{
925
    if (m_metadata->recycleBinEnabled()) {
926
        if (!m_metadata->recycleBin()) {
927
            createRecycleBin();
928
        }
929
        group->setParent(metadata()->recycleBin());
930
    } else {
931
        delete group;
932
    }
933
}
934

935
void Database::emptyRecycleBin()
936
{
937
    if (m_metadata->recycleBinEnabled() && m_metadata->recycleBin()) {
938
        // destroying direct entries of the recycle bin
939
        QList<Entry*> subEntries = m_metadata->recycleBin()->entries();
940
        for (Entry* entry : subEntries) {
941
            delete entry;
942
        }
943
        // destroying direct subgroups of the recycle bin
944
        QList<Group*> subGroups = m_metadata->recycleBin()->children();
945
        for (Group* group : subGroups) {
946
            delete group;
947
        }
948
    }
949
}
950

951
bool Database::isModified() const
952
{
953
    return m_modified;
954
}
955

956
bool Database::hasNonDataChanges() const
957
{
958
    return m_hasNonDataChange;
959
}
960

961
void Database::markAsModified()
962
{
963
    m_modified = true;
964
    if (modifiedSignalEnabled() && !m_modifiedTimer.isActive()) {
965
        // Small time delay prevents numerous consecutive saves due to repeated signals
966
        startModifiedTimer();
967
    }
968
}
969

970
void Database::markAsClean()
971
{
972
    bool emitSignal = m_modified;
973
    m_modified = false;
974
    stopModifiedTimer();
975
    m_hasNonDataChange = false;
976
    if (emitSignal) {
977
        emit databaseSaved();
978
    }
979
}
980

981
void Database::markNonDataChange()
982
{
983
    m_hasNonDataChange = true;
984
    emit databaseNonDataChanged();
985
}
986

987
/**
988
 * @param uuid UUID of the database
989
 * @return pointer to the database or nullptr if no such database exists
990
 */
991
Database* Database::databaseByUuid(const QUuid& uuid)
992
{
993
    return s_uuidMap.value(uuid, nullptr);
994
}
995

996
QSharedPointer<const CompositeKey> Database::key() const
997
{
998
    return m_data.key;
999
}
1000

1001
QSharedPointer<Kdf> Database::kdf() const
1002
{
1003
    return m_data.kdf;
1004
}
1005

1006
void Database::setKdf(QSharedPointer<Kdf> kdf)
1007
{
1008
    m_data.kdf = std::move(kdf);
1009
    setFormatVersion(KeePass2Writer::kdbxVersionRequired(this, true, m_data.kdf.isNull()));
1010
}
1011

1012
bool Database::changeKdf(const QSharedPointer<Kdf>& kdf)
1013
{
1014
    kdf->randomizeSeed();
1015
    QByteArray transformedDatabaseKey;
1016
    if (!m_data.key) {
1017
        m_data.key = QSharedPointer<CompositeKey>::create();
1018
    }
1019
    if (!m_data.key->transform(*kdf, transformedDatabaseKey)) {
1020
        return false;
1021
    }
1022

1023
    setKdf(kdf);
1024
    m_data.transformedDatabaseKey->setRawKey(transformedDatabaseKey);
1025
    markAsModified();
1026

1027
    return true;
1028
}
1029

1030
// Prevent warning about QTimer not allowed to be started/stopped from other thread
1031
void Database::startModifiedTimer()
1032
{
1033
    QMetaObject::invokeMethod(&m_modifiedTimer, "start", Q_ARG(int, 150));
1034
}
1035

1036
// Prevent warning about QTimer not allowed to be started/stopped from other thread
1037
void Database::stopModifiedTimer()
1038
{
1039
    QMetaObject::invokeMethod(&m_modifiedTimer, "stop");
1040
}
1041

1042
QUuid Database::publicUuid()
1043
{
1044

1045
    if (!publicCustomData().contains("KPXC_PUBLIC_UUID")) {
1046
        publicCustomData().insert("KPXC_PUBLIC_UUID", QUuid::createUuid().toRfc4122());
1047
        markAsModified();
1048
    }
1049

1050
    return QUuid::fromRfc4122(publicCustomData()["KPXC_PUBLIC_UUID"].toByteArray());
1051
}
1052

Использование cookies

Мы используем файлы cookie в соответствии с Политикой конфиденциальности и Политикой использования cookies.

Нажимая кнопку «Принимаю», Вы даете АО «СберТех» согласие на обработку Ваших персональных данных в целях совершенствования нашего веб-сайта и Сервиса GitVerse, а также повышения удобства их использования.

Запретить использование cookies Вы можете самостоятельно в настройках Вашего браузера.