keepassxc

Форк
0
/
BrowserService.cpp 
1672 строки · 56.5 Кб
1
/*
2
 *  Copyright (C) 2024 KeePassXC Team <team@keepassxc.org>
3
 *  Copyright (C) 2017 Sami Vänttinen <sami.vanttinen@protonmail.com>
4
 *  Copyright (C) 2013 Francois Ferrand
5
 *
6
 *  This program is free software: you can redistribute it and/or modify
7
 *  it under the terms of the GNU General Public License as published by
8
 *  the Free Software Foundation, either version 3 of the License, or
9
 *  (at your option) any later version.
10
 *
11
 *  This program 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 <http://www.gnu.org/licenses/>.
18
 */
19

20
#include "BrowserService.h"
21
#include "BrowserAction.h"
22
#include "BrowserEntryConfig.h"
23
#include "BrowserEntrySaveDialog.h"
24
#include "BrowserHost.h"
25
#include "BrowserMessageBuilder.h"
26
#include "BrowserSettings.h"
27
#include "core/Tools.h"
28
#include "core/UrlTools.h"
29
#include "gui/MainWindow.h"
30
#include "gui/MessageBox.h"
31
#include "gui/osutils/OSUtils.h"
32
#ifdef WITH_XC_BROWSER_PASSKEYS
33
#include "BrowserPasskeys.h"
34
#include "BrowserPasskeysClient.h"
35
#include "BrowserPasskeysConfirmationDialog.h"
36
#include "PasskeyUtils.h"
37
#include "gui/passkeys/PasskeyImporter.h"
38
#endif
39
#ifdef Q_OS_MACOS
40
#include "gui/osutils/macutils/MacUtils.h"
41
#endif
42

43
#include <QCheckBox>
44
#include <QCryptographicHash>
45
#include <QHostAddress>
46
#include <QInputDialog>
47
#include <QJsonArray>
48
#include <QJsonObject>
49
#include <QListWidget>
50
#include <QLocalSocket>
51
#include <QProgressDialog>
52
#include <QUrl>
53

54
const QString BrowserService::KEEPASSXCBROWSER_NAME = QStringLiteral("KeePassXC-Browser Settings");
55
const QString BrowserService::KEEPASSXCBROWSER_OLD_NAME = QStringLiteral("keepassxc-browser Settings");
56
static const QString KEEPASSXCBROWSER_GROUP_NAME = QStringLiteral("KeePassXC-Browser Passwords");
57
static int KEEPASSXCBROWSER_DEFAULT_ICON = 1;
58
#ifdef WITH_XC_BROWSER_PASSKEYS
59
static int KEEPASSXCBROWSER_PASSKEY_ICON = 13;
60
#endif
61
// These are for the settings and password conversion
62
static const QString KEEPASSHTTP_NAME = QStringLiteral("KeePassHttp Settings");
63
static const QString KEEPASSHTTP_GROUP_NAME = QStringLiteral("KeePassHttp Passwords");
64
// Extra entry related options saved in custom data
65
const QString BrowserService::OPTION_SKIP_AUTO_SUBMIT = QStringLiteral("BrowserSkipAutoSubmit");
66
const QString BrowserService::OPTION_HIDE_ENTRY = QStringLiteral("BrowserHideEntry");
67
const QString BrowserService::OPTION_ONLY_HTTP_AUTH = QStringLiteral("BrowserOnlyHttpAuth");
68
const QString BrowserService::OPTION_NOT_HTTP_AUTH = QStringLiteral("BrowserNotHttpAuth");
69
const QString BrowserService::OPTION_OMIT_WWW = QStringLiteral("BrowserOmitWww");
70
const QString BrowserService::OPTION_RESTRICT_KEY = QStringLiteral("BrowserRestrictKey");
71

72
Q_GLOBAL_STATIC(BrowserService, s_browserService);
73

74
BrowserService::BrowserService()
75
    : QObject()
76
    , m_browserHost(new BrowserHost)
77
    , m_dialogActive(false)
78
    , m_bringToFrontRequested(false)
79
    , m_prevWindowState(WindowState::Normal)
80
    , m_keepassBrowserUUID(Tools::hexToUuid("de887cc3036343b8974b5911b8816224"))
81
{
82
    connect(m_browserHost, &BrowserHost::clientMessageReceived, this, &BrowserService::processClientMessage);
83
    connect(getMainWindow(), &MainWindow::databaseUnlocked, this, &BrowserService::databaseUnlocked);
84
    connect(getMainWindow(), &MainWindow::databaseLocked, this, &BrowserService::databaseLocked);
85
    connect(getMainWindow(), &MainWindow::activeDatabaseChanged, this, &BrowserService::activeDatabaseChanged);
86

87
    setEnabled(browserSettings()->isEnabled());
88
}
89

90
BrowserService* BrowserService::instance()
91
{
92
    return s_browserService;
93
}
94

95
void BrowserService::setEnabled(bool enabled)
96
{
97
    if (enabled) {
98
        // Update KeePassXC/keepassxc-proxy binary paths to Native Messaging scripts
99
        if (browserSettings()->updateBinaryPath()) {
100
            browserSettings()->updateBinaryPaths();
101
        }
102

103
        m_browserHost->start();
104
    } else {
105
        m_browserHost->stop();
106
    }
107
}
108

109
bool BrowserService::isDatabaseOpened() const
110
{
111
    if (m_currentDatabaseWidget) {
112
        return !m_currentDatabaseWidget->isLocked();
113
    }
114
    return false;
115
}
116

117
bool BrowserService::openDatabase(bool triggerUnlock)
118
{
119
    if (!browserSettings()->unlockDatabase()) {
120
        return false;
121
    }
122

123
    if (m_currentDatabaseWidget && !m_currentDatabaseWidget->isLocked()) {
124
        return true;
125
    }
126

127
    if (triggerUnlock && !m_bringToFrontRequested) {
128
        m_bringToFrontRequested = true;
129
        updateWindowState();
130
        emit requestUnlock();
131
    }
132

133
    return false;
134
}
135

136
void BrowserService::lockDatabase()
137
{
138
    if (m_currentDatabaseWidget) {
139
        m_currentDatabaseWidget->lock();
140
    }
141
}
142

143
QString BrowserService::getDatabaseHash(bool legacy)
144
{
145
    if (legacy) {
146
        return QCryptographicHash::hash(
147
                   (browserService()->getDatabaseRootUuid() + browserService()->getDatabaseRecycleBinUuid()).toUtf8(),
148
                   QCryptographicHash::Sha256)
149
            .toHex();
150
    }
151
    return QCryptographicHash::hash(getDatabaseRootUuid().toUtf8(), QCryptographicHash::Sha256).toHex();
152
}
153

154
QString BrowserService::getDatabaseRootUuid()
155
{
156
    auto db = getDatabase();
157
    if (!db) {
158
        return {};
159
    }
160

161
    Group* rootGroup = db->rootGroup();
162
    if (!rootGroup) {
163
        return {};
164
    }
165

166
    return rootGroup->uuidToHex();
167
}
168

169
QString BrowserService::getDatabaseRecycleBinUuid()
170
{
171
    auto db = getDatabase();
172
    if (!db) {
173
        return {};
174
    }
175

176
    Group* recycleBin = db->metadata()->recycleBin();
177
    if (!recycleBin) {
178
        return {};
179
    }
180
    return recycleBin->uuidToHex();
181
}
182

183
QJsonArray BrowserService::getChildrenFromGroup(Group* group)
184
{
185
    QJsonArray groupList;
186

187
    if (!group) {
188
        return groupList;
189
    }
190

191
    for (const auto& c : group->children()) {
192
        if (c == group->database()->metadata()->recycleBin()) {
193
            continue;
194
        }
195

196
        QJsonObject jsonGroup;
197
        jsonGroup["name"] = c->name();
198
        jsonGroup["uuid"] = Tools::uuidToHex(c->uuid());
199
        jsonGroup["children"] = getChildrenFromGroup(c);
200
        groupList.push_back(jsonGroup);
201
    }
202
    return groupList;
203
}
204

205
QJsonObject BrowserService::getDatabaseGroups()
206
{
207
    auto db = getDatabase();
208
    if (!db) {
209
        return {};
210
    }
211

212
    Group* rootGroup = db->rootGroup();
213
    if (!rootGroup) {
214
        return {};
215
    }
216

217
    QJsonObject root;
218
    root["name"] = rootGroup->name();
219
    root["uuid"] = Tools::uuidToHex(rootGroup->uuid());
220
    root["children"] = getChildrenFromGroup(rootGroup);
221

222
    QJsonArray groups;
223
    groups.push_back(root);
224

225
    QJsonObject result;
226
    result["groups"] = groups;
227

228
    return result;
229
}
230

231
QJsonArray BrowserService::getDatabaseEntries()
232
{
233
    auto db = getDatabase();
234
    if (!db) {
235
        return {};
236
    }
237

238
    Group* rootGroup = db->rootGroup();
239
    if (!rootGroup) {
240
        return {};
241
    }
242

243
    QJsonArray entries;
244
    for (const auto& group : rootGroup->groupsRecursive(true)) {
245
        if (group == db->metadata()->recycleBin()) {
246
            continue;
247
        }
248

249
        for (const auto& entry : group->entries()) {
250
            QJsonObject jentry;
251
            jentry["title"] = entry->resolveMultiplePlaceholders(entry->title());
252
            jentry["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex());
253
            jentry["url"] = entry->resolveMultiplePlaceholders(entry->url());
254
            entries.push_back(jentry);
255
        }
256
    }
257
    return entries;
258
}
259

260
QJsonObject BrowserService::createNewGroup(const QString& groupName)
261
{
262
    auto db = getDatabase();
263
    if (!db) {
264
        return {};
265
    }
266

267
    Group* rootGroup = db->rootGroup();
268
    if (!rootGroup) {
269
        return {};
270
    }
271

272
    auto group = rootGroup->findGroupByPath(groupName);
273

274
    // Group already exists
275
    if (group) {
276
        QJsonObject result;
277
        result["name"] = group->name();
278
        result["uuid"] = Tools::uuidToHex(group->uuid());
279
        return result;
280
    }
281

282
    auto dialogResult = MessageBox::warning(m_currentDatabaseWidget,
283
                                            tr("KeePassXC - Create a new group"),
284
                                            tr("A request for creating a new group \"%1\" has been received.\n"
285
                                               "Do you want to create this group?\n")
286
                                                .arg(groupName),
287
                                            MessageBox::Yes | MessageBox::No);
288

289
    if (dialogResult != MessageBox::Yes) {
290
        return {};
291
    }
292

293
    QString name, uuid;
294
    Group* previousGroup = rootGroup;
295
    auto groups = groupName.split("/");
296

297
    // Returns the group name based on depth
298
    auto getGroupName = [&](int depth) {
299
        QString gName;
300
        for (int i = 0; i < depth + 1; ++i) {
301
            gName.append((i == 0 ? "" : "/") + groups[i]);
302
        }
303
        return gName;
304
    };
305

306
    // Create new group(s) always when the path is not found
307
    for (int i = 0; i < groups.length(); ++i) {
308
        QString gName = getGroupName(i);
309
        auto tempGroup = rootGroup->findGroupByPath(gName);
310
        if (!tempGroup) {
311
            Group* newGroup = new Group();
312
            newGroup->setName(groups[i]);
313
            newGroup->setUuid(QUuid::createUuid());
314
            newGroup->setParent(previousGroup);
315
            name = newGroup->name();
316
            uuid = Tools::uuidToHex(newGroup->uuid());
317
            previousGroup = newGroup;
318
            continue;
319
        }
320

321
        previousGroup = tempGroup;
322
    }
323

324
    QJsonObject result;
325
    result["name"] = name;
326
    result["uuid"] = uuid;
327
    return result;
328
}
329

330
QString BrowserService::getCurrentTotp(const QString& uuid)
331
{
332
    QList<QSharedPointer<Database>> databases;
333
    if (browserSettings()->searchInAllDatabases()) {
334
        for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
335
            auto db = dbWidget->database();
336
            if (db) {
337
                databases << db;
338
            }
339
        }
340
    } else {
341
        databases << getDatabase();
342
    }
343

344
    auto entryUuid = Tools::hexToUuid(uuid);
345
    for (const auto& db : databases) {
346
        auto entry = db->rootGroup()->findEntryByUuid(entryUuid, true);
347
        if (entry) {
348
            return entry->totp();
349
        }
350
    }
351

352
    return {};
353
}
354

355
QJsonArray
356
BrowserService::findEntries(const EntryParameters& entryParameters, const StringPairList& keyList, bool* entriesFound)
357
{
358
    if (entriesFound) {
359
        *entriesFound = false;
360
    }
361

362
    const bool alwaysAllowAccess = browserSettings()->alwaysAllowAccess();
363
    const bool ignoreHttpAuth = browserSettings()->httpAuthPermission();
364
    const QString siteHost = QUrl(entryParameters.siteUrl).host();
365
    const QString formHost = QUrl(entryParameters.formUrl).host();
366

367
    // Check entries for authorization
368
    QList<Entry*> entriesToConfirm;
369
    QList<Entry*> allowedEntries;
370
    for (auto* entry : searchEntries(entryParameters.siteUrl, entryParameters.formUrl, keyList)) {
371
        auto entryCustomData = entry->customData();
372

373
        if (!entryParameters.httpAuth
374
            && ((entryCustomData->contains(BrowserService::OPTION_ONLY_HTTP_AUTH)
375
                 && entryCustomData->value(BrowserService::OPTION_ONLY_HTTP_AUTH) == TRUE_STR)
376
                || entry->group()->resolveCustomDataTriState(BrowserService::OPTION_ONLY_HTTP_AUTH) == Group::Enable)) {
377
            continue;
378
        }
379

380
        if (entryParameters.httpAuth
381
            && ((entryCustomData->contains(BrowserService::OPTION_NOT_HTTP_AUTH)
382
                 && entryCustomData->value(BrowserService::OPTION_NOT_HTTP_AUTH) == TRUE_STR)
383
                || entry->group()->resolveCustomDataTriState(BrowserService::OPTION_NOT_HTTP_AUTH) == Group::Enable)) {
384
            continue;
385
        }
386

387
        // HTTP Basic Auth always needs a confirmation
388
        if (!ignoreHttpAuth && entryParameters.httpAuth) {
389
            entriesToConfirm.append(entry);
390
            continue;
391
        }
392

393
        switch (checkAccess(entry, siteHost, formHost, entryParameters.realm)) {
394
        case Denied:
395
            continue;
396

397
        case Unknown:
398
            if (alwaysAllowAccess) {
399
                allowedEntries.append(entry);
400
            } else {
401
                entriesToConfirm.append(entry);
402
            }
403
            break;
404

405
        case Allowed:
406
            allowedEntries.append(entry);
407
            break;
408
        }
409
    }
410

411
    if (entriesToConfirm.isEmpty() && allowedEntries.isEmpty()) {
412
        return {};
413
    }
414

415
    // Confirm entries
416
    auto selectedEntriesToConfirm =
417
        confirmEntries(entriesToConfirm, entryParameters, siteHost, formHost, entryParameters.httpAuth);
418
    if (!selectedEntriesToConfirm.isEmpty()) {
419
        allowedEntries.append(selectedEntriesToConfirm);
420
    }
421

422
    // Ensure that database is not locked when the popup was visible
423
    if (!isDatabaseOpened()) {
424
        return {};
425
    }
426

427
    // Sort results
428
    allowedEntries = sortEntries(allowedEntries, entryParameters.siteUrl, entryParameters.formUrl);
429

430
    // Fill the list
431
    QJsonArray entries;
432
    for (auto* entry : allowedEntries) {
433
        entries.append(prepareEntry(entry));
434
    }
435

436
    if (entriesFound != nullptr) {
437
        *entriesFound = true;
438
    }
439

440
    return entries;
441
}
442

443
QList<Entry*> BrowserService::confirmEntries(QList<Entry*>& entriesToConfirm,
444
                                             const EntryParameters& entryParameters,
445
                                             const QString& siteHost,
446
                                             const QString& formUrl,
447
                                             const bool httpAuth)
448
{
449
    if (entriesToConfirm.isEmpty() || m_dialogActive) {
450
        return {};
451
    }
452

453
    m_dialogActive = true;
454
    updateWindowState();
455
    BrowserAccessControlDialog accessControlDialog(m_currentDatabaseWidget);
456

457
    connect(m_currentDatabaseWidget, SIGNAL(databaseLockRequested()), &accessControlDialog, SLOT(reject()));
458

459
    connect(&accessControlDialog, &BrowserAccessControlDialog::disableAccess, [&](QTableWidgetItem* item) {
460
        auto entry = entriesToConfirm[item->row()];
461
        denyEntry(entry, siteHost, formUrl, entryParameters.realm);
462
    });
463

464
    accessControlDialog.setEntries(entriesToConfirm, entryParameters.siteUrl, httpAuth);
465

466
    QList<Entry*> allowedEntries;
467
    auto ret = accessControlDialog.exec();
468
    auto remember = accessControlDialog.remember();
469

470
    // All are denied
471
    if (ret == QDialog::Rejected && remember) {
472
        for (auto& entry : entriesToConfirm) {
473
            denyEntry(entry, siteHost, formUrl, entryParameters.realm);
474
        }
475
    }
476

477
    // Some/all are accepted
478
    if (ret == QDialog::Accepted) {
479
        auto selectedEntries = accessControlDialog.getEntries(SelectionType::Selected);
480
        for (auto& item : selectedEntries) {
481
            auto entry = entriesToConfirm[item->row()];
482
            allowedEntries.append(entry);
483

484
            if (remember) {
485
                allowEntry(entry, siteHost, formUrl, entryParameters.realm);
486
            }
487
        }
488

489
        // Remembered non-selected entries must be denied
490
        if (remember) {
491
            auto nonSelectedEntries = accessControlDialog.getEntries(SelectionType::NonSelected);
492
            for (auto& item : nonSelectedEntries) {
493
                auto entry = entriesToConfirm[item->row()];
494
                denyEntry(entry, siteHost, formUrl, entryParameters.realm);
495
            }
496
        }
497
    }
498

499
    // Handle disabled entries (returned Accept/Reject status does not matter)
500
    auto disabledEntries = accessControlDialog.getEntries(SelectionType::Disabled);
501
    for (auto& item : disabledEntries) {
502
        auto entry = entriesToConfirm[item->row()];
503
        denyEntry(entry, siteHost, formUrl, entryParameters.realm);
504
    }
505

506
    // Re-hide the application if it wasn't visible before
507
    hideWindow();
508
    m_dialogActive = false;
509

510
    return allowedEntries;
511
}
512

513
void BrowserService::showPasswordGenerator(const KeyPairMessage& keyPairMessage)
514
{
515
    if (!m_passwordGenerator) {
516
        m_passwordGenerator = PasswordGeneratorWidget::popupGenerator();
517

518
        connect(m_passwordGenerator.data(), &PasswordGeneratorWidget::closed, m_passwordGenerator.data(), [=] {
519
            if (!m_passwordGenerator->isPasswordGenerated()) {
520
                auto errorMessage = browserMessageBuilder()->getErrorReply("generate-password",
521
                                                                           ERROR_KEEPASS_ACTION_CANCELLED_OR_DENIED);
522
                m_browserHost->sendClientMessage(keyPairMessage.socket, errorMessage);
523
            }
524

525
            QTimer::singleShot(50, this, [&] { hideWindow(); });
526
        });
527

528
        connect(m_passwordGenerator.data(),
529
                &PasswordGeneratorWidget::appliedPassword,
530
                m_passwordGenerator.data(),
531
                [=](const QString& password) {
532
                    const Parameters params{{"password", password}};
533
                    m_browserHost->sendClientMessage(keyPairMessage.socket,
534
                                                     browserMessageBuilder()->buildResponse("generate-password",
535
                                                                                            keyPairMessage.nonce,
536
                                                                                            params,
537
                                                                                            keyPairMessage.publicKey,
538
                                                                                            keyPairMessage.secretKey));
539
                });
540
    }
541

542
    raiseWindow();
543
    m_passwordGenerator->show();
544
    m_passwordGenerator->raise();
545
    m_passwordGenerator->activateWindow();
546
}
547

548
bool BrowserService::isPasswordGeneratorRequested() const
549
{
550
    return m_passwordGenerator && m_passwordGenerator->isVisible();
551
}
552

553
QString BrowserService::storeKey(const QString& key)
554
{
555
    auto db = getDatabase();
556
    if (!db) {
557
        return {};
558
    }
559

560
    bool contains;
561
    auto dialogResult = MessageBox::Cancel;
562
    QString id;
563

564
    do {
565
        QInputDialog keyDialog(m_currentDatabaseWidget);
566
        connect(m_currentDatabaseWidget, SIGNAL(databaseLockRequested()), &keyDialog, SLOT(reject()));
567
        keyDialog.setWindowTitle(tr("KeePassXC - New key association request"));
568
        keyDialog.setLabelText(tr("You have received an association request for the following database:\n%1\n\n"
569
                                  "Give the connection a unique name or ID, for example:\nchrome-laptop.")
570
                                   .arg(db->metadata()->name().toHtmlEscaped()));
571
        keyDialog.setOkButtonText(tr("Save and allow access"));
572
        keyDialog.setWindowFlags(keyDialog.windowFlags() | Qt::WindowStaysOnTopHint);
573
        raiseWindow();
574
        keyDialog.show();
575
        keyDialog.activateWindow();
576
        keyDialog.raise();
577
        auto ok = keyDialog.exec();
578

579
        id = keyDialog.textValue();
580

581
        if (ok != QDialog::Accepted || id.isEmpty() || !isDatabaseOpened()) {
582
            hideWindow();
583
            return {};
584
        }
585

586
        contains = db->metadata()->customData()->contains(CustomData::BrowserKeyPrefix + id);
587
        if (contains) {
588
            dialogResult = MessageBox::warning(m_currentDatabaseWidget,
589
                                               tr("KeePassXC - Overwrite existing key?"),
590
                                               tr("A shared encryption key with the name \"%1\" "
591
                                                  "already exists.\nDo you want to overwrite it?")
592
                                                   .arg(id),
593
                                               MessageBox::Overwrite | MessageBox::Cancel,
594
                                               MessageBox::Cancel);
595
        }
596
    } while (contains && dialogResult == MessageBox::Cancel);
597

598
    hideWindow();
599
    db->metadata()->customData()->set(CustomData::BrowserKeyPrefix + id, key);
600
    db->metadata()->customData()->set(QString("%1_%2").arg(CustomData::Created, id),
601
                                      Clock::currentDateTime().toString(Qt::SystemLocaleShortDate));
602
    return id;
603
}
604

605
QString BrowserService::getKey(const QString& id)
606
{
607
    auto db = getDatabase();
608
    if (!db) {
609
        return {};
610
    }
611

612
    return db->metadata()->customData()->value(CustomData::BrowserKeyPrefix + id);
613
}
614

615
#ifdef WITH_XC_BROWSER_PASSKEYS
616
// Passkey registration
617
QJsonObject BrowserService::showPasskeysRegisterPrompt(const QJsonObject& publicKeyOptions,
618
                                                       const QString& origin,
619
                                                       const StringPairList& keyList)
620
{
621
    auto db = selectedDatabase();
622
    if (!db) {
623
        return getPasskeyError(ERROR_KEEPASS_DATABASE_NOT_OPENED);
624
    }
625

626
    QJsonObject credentialCreationOptions;
627
    const auto pkOptionsResult =
628
        browserPasskeysClient()->getCredentialCreationOptions(publicKeyOptions, origin, &credentialCreationOptions);
629
    if (pkOptionsResult > 0 || credentialCreationOptions.isEmpty()) {
630
        return getPasskeyError(pkOptionsResult);
631
    }
632

633
    const auto excludeCredentials = credentialCreationOptions["excludeCredentials"].toArray();
634
    const auto rpId = credentialCreationOptions["rp"].toObject()["id"].toString();
635
    const auto timeout = publicKeyOptions["timeout"].toInt();
636
    const auto username = credentialCreationOptions["user"].toObject()["name"].toString();
637
    const auto user = credentialCreationOptions["user"].toObject();
638
    const auto userId = user["id"].toString();
639

640
    // Parse excludeCredentialDescriptorList
641
    if (!excludeCredentials.isEmpty() && isPasskeyCredentialExcluded(excludeCredentials, rpId, keyList)) {
642
        return getPasskeyError(ERROR_PASSKEYS_CREDENTIAL_IS_EXCLUDED);
643
    }
644

645
    const auto existingEntries = getPasskeyEntriesWithUserHandle(rpId, userId, keyList);
646

647
    raiseWindow();
648
    BrowserPasskeysConfirmationDialog confirmDialog(m_currentDatabaseWidget);
649
    confirmDialog.registerCredential(username, rpId, existingEntries, timeout);
650

651
    auto dialogResult = confirmDialog.exec();
652
    if (dialogResult == QDialog::Accepted) {
653
        const auto publicKeyCredentials =
654
            browserPasskeys()->buildRegisterPublicKeyCredential(credentialCreationOptions);
655
        if (publicKeyCredentials.credentialId.isEmpty() || publicKeyCredentials.key.isEmpty()
656
            || publicKeyCredentials.response.isEmpty()) {
657
            return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR);
658
        }
659

660
        const auto rpName = publicKeyOptions["rp"]["name"].toString();
661
        if (confirmDialog.isPasskeyUpdated()) {
662
            // If no entry is selected, show the import dialog for manual entry selection
663
            auto selectedEntry = confirmDialog.getSelectedEntry();
664
            if (!selectedEntry) {
665
                PasskeyImporter passkeyImporter(m_currentDatabaseWidget);
666
                const auto result = passkeyImporter.showImportDialog(db,
667
                                                                     nullptr,
668
                                                                     origin,
669
                                                                     rpId,
670
                                                                     username,
671
                                                                     publicKeyCredentials.credentialId,
672
                                                                     userId,
673
                                                                     publicKeyCredentials.key,
674
                                                                     tr("KeePassXC - Passkey credentials"),
675
                                                                     tr("Register a new passkey to this entry:"),
676
                                                                     tr("Register"));
677
                if (!result) {
678
                    return getPasskeyError(ERROR_PASSKEYS_REQUEST_CANCELED);
679
                }
680
            } else {
681
                addPasskeyToEntry(selectedEntry,
682
                                  rpId,
683
                                  rpName,
684
                                  username,
685
                                  publicKeyCredentials.credentialId,
686
                                  userId,
687
                                  publicKeyCredentials.key);
688
            }
689
        } else {
690
            addPasskeyToGroup(db,
691
                              nullptr,
692
                              origin,
693
                              rpId,
694
                              rpName,
695
                              username,
696
                              publicKeyCredentials.credentialId,
697
                              userId,
698
                              publicKeyCredentials.key);
699
        }
700

701
        hideWindow();
702
        return publicKeyCredentials.response;
703
    }
704

705
    hideWindow();
706
    return getPasskeyError(ERROR_PASSKEYS_REQUEST_CANCELED);
707
}
708

709
// Passkey authentication
710
QJsonObject BrowserService::showPasskeysAuthenticationPrompt(const QJsonObject& publicKeyOptions,
711
                                                             const QString& origin,
712
                                                             const StringPairList& keyList)
713
{
714
    auto db = getDatabase();
715
    if (!db) {
716
        return getPasskeyError(ERROR_KEEPASS_DATABASE_NOT_OPENED);
717
    }
718

719
    QJsonObject assertionOptions;
720
    const auto assertionResult =
721
        browserPasskeysClient()->getAssertionOptions(publicKeyOptions, origin, &assertionOptions);
722
    if (assertionResult > 0 || assertionOptions.isEmpty()) {
723
        return getPasskeyError(assertionResult);
724
    }
725

726
    // Get allowed entries from RP ID
727
    const auto rpId = assertionOptions["rpId"].toString();
728
    const auto entries = getPasskeyAllowedEntries(assertionOptions, rpId, keyList);
729
    if (entries.isEmpty()) {
730
        return getPasskeyError(ERROR_KEEPASS_NO_LOGINS_FOUND);
731
    }
732

733
    const auto timeout = publicKeyOptions["timeout"].toInt();
734

735
    raiseWindow();
736
    BrowserPasskeysConfirmationDialog confirmDialog(m_currentDatabaseWidget);
737
    confirmDialog.authenticateCredential(entries, rpId, timeout);
738
    auto dialogResult = confirmDialog.exec();
739
    if (dialogResult == QDialog::Accepted) {
740
        hideWindow();
741
        const auto selectedEntry = confirmDialog.getSelectedEntry();
742
        if (!selectedEntry) {
743
            return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR);
744
        }
745

746
        const auto privateKeyPem = selectedEntry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM);
747
        const auto credentialId = passkeyUtils()->getCredentialIdFromEntry(selectedEntry);
748
        const auto userHandle = selectedEntry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE);
749

750
        auto publicKeyCredential =
751
            browserPasskeys()->buildGetPublicKeyCredential(assertionOptions, credentialId, userHandle, privateKeyPem);
752
        if (publicKeyCredential.isEmpty()) {
753
            return getPasskeyError(ERROR_PASSKEYS_UNKNOWN_ERROR);
754
        }
755

756
        return publicKeyCredential;
757
    }
758

759
    hideWindow();
760
    return getPasskeyError(ERROR_PASSKEYS_REQUEST_CANCELED);
761
}
762

763
void BrowserService::addPasskeyToGroup(const QSharedPointer<Database>& db,
764
                                       Group* group,
765
                                       const QString& url,
766
                                       const QString& rpId,
767
                                       const QString& rpName,
768
                                       const QString& username,
769
                                       const QString& credentialId,
770
                                       const QString& userHandle,
771
                                       const QString& privateKey)
772
{
773
    // If no group provided, use the default browser group of the selected database
774
    if (!group) {
775
        if (!db) {
776
            return;
777
        }
778
        group = getDefaultEntryGroup(db);
779
    }
780

781
    auto* entry = new Entry();
782
    entry->setUuid(QUuid::createUuid());
783
    entry->setGroup(group);
784
    entry->setTitle(tr("%1 (Passkey)").arg(rpName));
785
    entry->setUsername(username);
786
    entry->setUrl(url);
787
    entry->setIcon(KEEPASSXCBROWSER_PASSKEY_ICON);
788

789
    addPasskeyToEntry(entry, rpId, rpName, username, credentialId, userHandle, privateKey);
790

791
    // Remove blank entry history
792
    entry->removeHistoryItems(entry->historyItems());
793
}
794

795
void BrowserService::addPasskeyToEntry(Entry* entry,
796
                                       const QString& rpId,
797
                                       const QString& rpName,
798
                                       const QString& username,
799
                                       const QString& credentialId,
800
                                       const QString& userHandle,
801
                                       const QString& privateKey)
802
{
803
    // Reserved for future use
804
    Q_UNUSED(rpName)
805

806
    Q_ASSERT(entry);
807
    if (!entry) {
808
        return;
809
    }
810

811
    // Ask confirmation if entry already contains a Passkey
812
    if (entry->hasPasskey()) {
813
        if (MessageBox::question(m_currentDatabaseWidget,
814
                                 tr("KeePassXC - Update passkey"),
815
                                 tr("Entry already has a passkey.\nDo you want to overwrite the passkey in %1 - %2?")
816
                                     .arg(entry->title(), passkeyUtils()->getUsernameFromEntry(entry)),
817
                                 MessageBox::Overwrite | MessageBox::Cancel,
818
                                 MessageBox::Cancel)
819
            != MessageBox::Overwrite) {
820
            return;
821
        }
822
    }
823

824
    entry->beginUpdate();
825

826
    entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USERNAME, username);
827
    entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_CREDENTIAL_ID, credentialId, true);
828
    entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_PRIVATE_KEY_PEM, privateKey, true);
829
    entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY, rpId);
830
    entry->attributes()->set(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE, userHandle, true);
831
    entry->addTag(tr("Passkey"));
832

833
    entry->endUpdate();
834
}
835
#endif
836

837
void BrowserService::addEntry(const EntryParameters& entryParameters,
838
                              const QString& group,
839
                              const QString& groupUuid,
840
                              const bool downloadFavicon,
841
                              const QSharedPointer<Database>& selectedDb)
842
{
843
    // TODO: select database based on this key id
844
    auto db = selectedDb ? selectedDb : selectedDatabase();
845
    if (!db) {
846
        return;
847
    }
848

849
    auto* entry = new Entry();
850
    entry->setUuid(QUuid::createUuid());
851
    entry->setTitle(entryParameters.title.isEmpty() ? QUrl(entryParameters.siteUrl).host() : entryParameters.title);
852
    entry->setUrl(entryParameters.siteUrl);
853
    entry->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON);
854
    entry->setUsername(entryParameters.login);
855
    entry->setPassword(entryParameters.password);
856

857
    // Select a group for the entry
858
    if (!group.isEmpty()) {
859
        if (db->rootGroup()) {
860
            auto selectedGroup = db->rootGroup()->findGroupByUuid(Tools::hexToUuid(groupUuid));
861
            if (selectedGroup) {
862
                entry->setGroup(selectedGroup);
863
            } else {
864
                entry->setGroup(getDefaultEntryGroup(db));
865
            }
866
        }
867
    } else {
868
        entry->setGroup(getDefaultEntryGroup(db));
869
    }
870

871
    const QString host = QUrl(entryParameters.siteUrl).host();
872
    const QString submitHost = QUrl(entryParameters.formUrl).host();
873
    BrowserEntryConfig config;
874
    config.allow(host);
875

876
    if (!submitHost.isEmpty()) {
877
        config.allow(submitHost);
878
    }
879
    if (!entryParameters.realm.isEmpty()) {
880
        config.setRealm(entryParameters.realm);
881
    }
882
    config.save(entry);
883

884
    if (downloadFavicon && m_currentDatabaseWidget) {
885
        m_currentDatabaseWidget->downloadFaviconInBackground(entry);
886
    }
887
}
888

889
bool BrowserService::updateEntry(const EntryParameters& entryParameters, const QString& uuid)
890
{
891
    // TODO: select database based on this key id
892
    auto db = selectedDatabase();
893
    if (!db) {
894
        return false;
895
    }
896

897
    auto entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid));
898
    if (!entry) {
899
        // If entry is not found for update, add a new one to the selected database
900
        addEntry(entryParameters, "", "", false, db);
901
        return true;
902
    }
903

904
    // Check if the entry password is a reference. If so, update the original entry instead
905
    while (entry->attributes()->isReference(EntryAttributes::PasswordKey)) {
906
        const QUuid referenceUuid = entry->attributes()->referenceUuid(EntryAttributes::PasswordKey);
907
        if (!referenceUuid.isNull()) {
908
            entry = db->rootGroup()->findEntryByUuid(referenceUuid);
909
            if (!entry) {
910
                return false;
911
            }
912
        }
913
    }
914

915
    auto username = entry->username();
916
    if (username.isEmpty()) {
917
        return false;
918
    }
919

920
    bool result = false;
921
    if (username.compare(entryParameters.login, Qt::CaseSensitive) != 0
922
        || entry->password().compare(entryParameters.password, Qt::CaseSensitive) != 0) {
923
        MessageBox::Button dialogResult = MessageBox::No;
924
        if (!browserSettings()->alwaysAllowUpdate()) {
925
            raiseWindow();
926
            dialogResult = MessageBox::question(m_currentDatabaseWidget,
927
                                                tr("KeePassXC - Update Entry"),
928
                                                tr("Do you want to update the information in %1 - %2?")
929
                                                    .arg(QUrl(entryParameters.siteUrl).host(), username),
930
                                                MessageBox::Save | MessageBox::Cancel,
931
                                                MessageBox::Cancel);
932
        }
933

934
        if (browserSettings()->alwaysAllowUpdate() || dialogResult == MessageBox::Save) {
935
            entry->beginUpdate();
936
            if (!entry->attributes()->isReference(EntryAttributes::UserNameKey)) {
937
                entry->setUsername(entryParameters.login);
938
            }
939
            entry->setPassword(entryParameters.password);
940
            entry->endUpdate();
941
            result = true;
942
        }
943

944
        hideWindow();
945
    }
946

947
    return result;
948
}
949

950
bool BrowserService::deleteEntry(const QString& uuid)
951
{
952
    auto db = selectedDatabase();
953
    if (!db) {
954
        return false;
955
    }
956

957
    auto* entry = db->rootGroup()->findEntryByUuid(Tools::hexToUuid(uuid));
958
    if (!entry) {
959
        return false;
960
    }
961

962
    auto dialogResult = MessageBox::warning(m_currentDatabaseWidget,
963
                                            tr("KeePassXC - Delete entry"),
964
                                            tr("A request for deleting entry \"%1\" has been received.\n"
965
                                               "Do you want to delete the entry?\n")
966
                                                .arg(entry->title()),
967
                                            MessageBox::Yes | MessageBox::No);
968
    if (dialogResult != MessageBox::Yes) {
969
        return false;
970
    }
971

972
    db->recycleEntry(entry);
973
    return true;
974
}
975

976
QList<Entry*> BrowserService::searchEntries(const QSharedPointer<Database>& db,
977
                                            const QString& siteUrl,
978
                                            const QString& formUrl,
979
                                            const QStringList& keys,
980
                                            bool passkey)
981
{
982
    QList<Entry*> entries;
983
    auto* rootGroup = db->rootGroup();
984
    if (!rootGroup) {
985
        return entries;
986
    }
987

988
    for (const auto& group : rootGroup->groupsRecursive(true)) {
989
        if (group->isRecycled()
990
            || group->resolveCustomDataTriState(BrowserService::OPTION_HIDE_ENTRY) == Group::Enable) {
991
            continue;
992
        }
993

994
        // If a key restriction is specified and not contained in the keys list then skip this group.
995
        auto restrictKey = group->resolveCustomDataString(BrowserService::OPTION_RESTRICT_KEY);
996
        if (!restrictKey.isEmpty() && !keys.contains(restrictKey)) {
997
            continue;
998
        }
999

1000
        const auto omitWwwSubdomain =
1001
            group->resolveCustomDataTriState(BrowserService::OPTION_OMIT_WWW) == Group::Enable;
1002

1003
        for (auto* entry : group->entries()) {
1004
            if (entry->isRecycled()
1005
                || (entry->customData()->contains(BrowserService::OPTION_HIDE_ENTRY)
1006
                    && entry->customData()->value(BrowserService::OPTION_HIDE_ENTRY) == TRUE_STR)) {
1007
                continue;
1008
            }
1009

1010
            if (!passkey && !shouldIncludeEntry(entry, siteUrl, formUrl, omitWwwSubdomain)) {
1011
                continue;
1012
            }
1013

1014
#ifdef WITH_XC_BROWSER_PASSKEYS
1015
            // With Passkeys, check for the Relying Party instead of URL
1016
            if (passkey && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) != siteUrl) {
1017
                continue;
1018
            }
1019
#endif
1020

1021
            // Additional URL check may have already inserted the entry to the list
1022
            if (!entries.contains(entry)) {
1023
                entries.append(entry);
1024
            }
1025
        }
1026
    }
1027

1028
    return entries;
1029
}
1030

1031
QList<Entry*> BrowserService::searchEntries(const QString& siteUrl,
1032
                                            const QString& formUrl,
1033
                                            const StringPairList& keyList,
1034
                                            bool passkey)
1035
{
1036
    // Check if database is connected with KeePassXC-Browser. If so, return browser key (otherwise empty)
1037
    auto databaseConnected = [&](const QSharedPointer<Database>& db) {
1038
        for (const StringPair& keyPair : keyList) {
1039
            QString key = db->metadata()->customData()->value(CustomData::BrowserKeyPrefix + keyPair.first);
1040
            if (!key.isEmpty() && keyPair.second == key) {
1041
                return keyPair.first;
1042
            }
1043
        }
1044
        return QString();
1045
    };
1046

1047
    // Get the list of databases to search
1048
    QList<QSharedPointer<Database>> databases;
1049
    QStringList keys;
1050
    if (browserSettings()->searchInAllDatabases()) {
1051
        for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
1052
            auto db = dbWidget->database();
1053
            auto key = databaseConnected(dbWidget->database());
1054
            if (db && !key.isEmpty()) {
1055
                databases << db;
1056
                keys << key;
1057
            }
1058
        }
1059
    } else {
1060
        const auto& db = getDatabase();
1061
        auto key = databaseConnected(db);
1062
        if (!key.isEmpty()) {
1063
            databases << db;
1064
            keys << key;
1065
        }
1066
    }
1067

1068
    // Search entries matching the hostname
1069
    QString hostname = QUrl(siteUrl).host();
1070
    QList<Entry*> entries;
1071
    do {
1072
        for (const auto& db : databases) {
1073
            entries << searchEntries(db, siteUrl, formUrl, keys, passkey);
1074
        }
1075
    } while (entries.isEmpty() && removeFirstDomain(hostname));
1076

1077
    return entries;
1078
}
1079

1080
QString BrowserService::decodeCustomDataRestrictKey(const QString& key)
1081
{
1082
    return key.isEmpty() ? tr("Disable") : key;
1083
}
1084

1085
void BrowserService::requestGlobalAutoType(const QString& search)
1086
{
1087
    emit osUtils->globalShortcutTriggered("autotype", search);
1088
}
1089

1090
QList<Entry*> BrowserService::sortEntries(QList<Entry*>& entries, const QString& siteUrl, const QString& formUrl)
1091
{
1092
    // Build map of prioritized entries
1093
    QMultiMap<int, Entry*> priorities;
1094
    for (auto* entry : entries) {
1095
        priorities.insert(sortPriority(entry->getAllUrls(), siteUrl, formUrl), entry);
1096
    }
1097

1098
    auto keys = priorities.uniqueKeys();
1099
    std::sort(keys.begin(), keys.end(), [](int l, int r) { return l > r; });
1100

1101
    QList<Entry*> results;
1102
    for (auto key : keys) {
1103
        results << priorities.values(key);
1104

1105
        if (browserSettings()->bestMatchOnly() && !results.isEmpty()) {
1106
            // Early out once we find the highest batch of matches
1107
            break;
1108
        }
1109
    }
1110

1111
    return results;
1112
}
1113

1114
void BrowserService::allowEntry(Entry* entry, const QString& siteHost, const QString& formUrl, const QString& realm)
1115
{
1116
    BrowserEntryConfig config;
1117
    config.load(entry);
1118
    config.allow(siteHost);
1119

1120
    if (!formUrl.isEmpty() && siteHost != formUrl) {
1121
        config.allow(formUrl);
1122
    }
1123

1124
    if (!realm.isEmpty()) {
1125
        config.setRealm(realm);
1126
    }
1127

1128
    config.save(entry);
1129
}
1130

1131
void BrowserService::denyEntry(Entry* entry, const QString& siteHost, const QString& formUrl, const QString& realm)
1132
{
1133
    BrowserEntryConfig config;
1134
    config.load(entry);
1135
    config.deny(siteHost);
1136

1137
    if (!formUrl.isEmpty() && siteHost != formUrl) {
1138
        config.deny(formUrl);
1139
    }
1140

1141
    if (!realm.isEmpty()) {
1142
        config.setRealm(realm);
1143
    }
1144

1145
    config.save(entry);
1146
}
1147

1148
QJsonObject BrowserService::prepareEntry(const Entry* entry)
1149
{
1150
    QJsonObject res;
1151
#ifdef WITH_XC_BROWSER_PASSKEYS
1152
    // Use Passkey's username instead if found
1153
    res["login"] = entry->hasPasskey() ? passkeyUtils()->getUsernameFromEntry(entry)
1154
                                       : entry->resolveMultiplePlaceholders(entry->username());
1155
#else
1156
    res["login"] = entry->resolveMultiplePlaceholders(entry->username());
1157
#endif
1158
    res["password"] = entry->resolveMultiplePlaceholders(entry->password());
1159
    res["name"] = entry->resolveMultiplePlaceholders(entry->title());
1160
    res["uuid"] = entry->resolveMultiplePlaceholders(entry->uuidToHex());
1161
    res["group"] = entry->resolveMultiplePlaceholders(entry->group()->name());
1162

1163
    if (entry->hasTotp()) {
1164
        res["totp"] = entry->totp();
1165
    }
1166

1167
    if (entry->isExpired()) {
1168
        res["expired"] = TRUE_STR;
1169
    }
1170

1171
    auto skipAutoSubmitGroup = entry->group()->resolveCustomDataTriState(BrowserService::OPTION_SKIP_AUTO_SUBMIT);
1172
    if (skipAutoSubmitGroup == Group::Inherit) {
1173
        if (entry->customData()->contains(BrowserService::OPTION_SKIP_AUTO_SUBMIT)) {
1174
            res["skipAutoSubmit"] = entry->customData()->value(BrowserService::OPTION_SKIP_AUTO_SUBMIT);
1175
        }
1176
    } else {
1177
        res["skipAutoSubmit"] = skipAutoSubmitGroup == Group::Enable ? TRUE_STR : FALSE_STR;
1178
    }
1179

1180
    if (browserSettings()->supportKphFields()) {
1181
        const EntryAttributes* attr = entry->attributes();
1182
        QJsonArray stringFields;
1183
        for (const auto& key : attr->keys()) {
1184
            if (key.startsWith("KPH: ")) {
1185
                QJsonObject sField;
1186
                sField[key] = entry->resolveMultiplePlaceholders(attr->value(key));
1187
                stringFields.append(sField);
1188
            }
1189
        }
1190
        res["stringFields"] = stringFields;
1191
    }
1192
    return res;
1193
}
1194

1195
BrowserService::Access
1196
BrowserService::checkAccess(const Entry* entry, const QString& siteHost, const QString& formHost, const QString& realm)
1197
{
1198
    if (entry->isExpired() && !browserSettings()->allowExpiredCredentials()) {
1199
        return Denied;
1200
    }
1201

1202
    BrowserEntryConfig config;
1203
    if (!config.load(entry)) {
1204
        return Unknown;
1205
    }
1206
    if ((config.isAllowed(siteHost)) && (formHost.isEmpty() || config.isAllowed(formHost))) {
1207
        return Allowed;
1208
    }
1209
    if ((config.isDenied(siteHost)) || (!formHost.isEmpty() && config.isDenied(formHost))) {
1210
        return Denied;
1211
    }
1212
    if (!realm.isEmpty() && config.realm() != realm) {
1213
        return Denied;
1214
    }
1215
    return Unknown;
1216
}
1217

1218
Group* BrowserService::getDefaultEntryGroup(const QSharedPointer<Database>& selectedDb)
1219
{
1220
    auto db = selectedDb ? selectedDb : getDatabase();
1221
    if (!db) {
1222
        return nullptr;
1223
    }
1224

1225
    auto* rootGroup = db->rootGroup();
1226
    if (!rootGroup) {
1227
        return nullptr;
1228
    }
1229

1230
    for (auto* g : rootGroup->groupsRecursive(true)) {
1231
        if (g->name() == KEEPASSXCBROWSER_GROUP_NAME && !g->isRecycled()) {
1232
            return db->rootGroup()->findGroupByUuid(g->uuid());
1233
        }
1234
    }
1235

1236
    auto* group = new Group();
1237
    group->setUuid(QUuid::createUuid());
1238
    group->setName(KEEPASSXCBROWSER_GROUP_NAME);
1239
    group->setIcon(KEEPASSXCBROWSER_DEFAULT_ICON);
1240
    group->setParent(rootGroup);
1241
    return group;
1242
}
1243

1244
// Returns the maximum sort priority given a set of match urls and the
1245
// extension provided site and form url.
1246
int BrowserService::sortPriority(const QStringList& urls, const QString& siteUrl, const QString& formUrl)
1247
{
1248
    QList<int> priorityList;
1249
    // NOTE: QUrl::matches is utterly broken in Qt < 5.11, so we work around that
1250
    // by removing parts of the url that we don't match and direct matching others
1251
    const auto stdOpts = QUrl::RemoveFragment | QUrl::RemoveUserInfo;
1252
    const auto adjustedSiteUrl = QUrl(siteUrl).adjusted(stdOpts);
1253
    const auto adjustedFormUrl = QUrl(formUrl).adjusted(stdOpts);
1254

1255
    auto getPriority = [&](const QString& givenUrl) {
1256
        auto url = QUrl::fromUserInput(givenUrl).adjusted(stdOpts);
1257

1258
        // Default to https scheme if undefined
1259
        if (url.scheme().isEmpty() || !givenUrl.contains("://")) {
1260
            url.setScheme("https");
1261
        }
1262

1263
        // Add the empty path to the URL if it's missing.
1264
        // URL's from the extension always have a path set, entry URL's can be without.
1265
        if (url.path().isEmpty() && !url.hasFragment() && !url.hasQuery()) {
1266
            url.setPath("/");
1267
        }
1268

1269
        // Reject invalid urls and hosts, except 'localhost', and scheme mismatch
1270
        if (!url.isValid() || (!url.host().contains(".") && url.host() != "localhost")
1271
            || url.scheme() != adjustedSiteUrl.scheme()) {
1272
            return 0;
1273
        }
1274

1275
        // Exact match with site url or form url
1276
        if (url.matches(adjustedSiteUrl, QUrl::None) || url.matches(adjustedFormUrl, QUrl::None)) {
1277
            return 100;
1278
        }
1279

1280
        // Exact match without the query string
1281
        if (url.matches(adjustedSiteUrl, QUrl::RemoveQuery) || url.matches(adjustedFormUrl, QUrl::RemoveQuery)) {
1282
            return 90;
1283
        }
1284

1285
        // Parent directory match
1286
        if (url.isParentOf(adjustedSiteUrl) || url.isParentOf(adjustedFormUrl)) {
1287
            return 85;
1288
        }
1289

1290
        // Match without path (ie, FQDN match), form url prioritizes lower than site url
1291
        if (url.host() == adjustedSiteUrl.host()) {
1292
            return 80;
1293
        }
1294
        if (url.host() == adjustedFormUrl.host()) {
1295
            return 70;
1296
        }
1297

1298
        // Site/form url ends with given url (subdomain mismatch)
1299
        if (adjustedSiteUrl.host().endsWith(url.host())) {
1300
            return 60;
1301
        }
1302
        if (adjustedFormUrl.host().endsWith(url.host())) {
1303
            return 50;
1304
        }
1305

1306
        // No valid match found
1307
        return 0;
1308
    };
1309

1310
    for (const auto& entryUrl : urls) {
1311
        priorityList << getPriority(entryUrl);
1312
    }
1313

1314
    return *std::max_element(priorityList.begin(), priorityList.end());
1315
}
1316

1317
bool BrowserService::removeFirstDomain(QString& hostname)
1318
{
1319
    int pos = hostname.indexOf(".");
1320
    if (pos < 0) {
1321
        return false;
1322
    }
1323

1324
    // Don't remove the second-level domain if it's the only one
1325
    if (hostname.count(".") > 1) {
1326
        hostname = hostname.mid(pos + 1);
1327
        return !hostname.isEmpty();
1328
    }
1329

1330
    // Nothing removed
1331
    return false;
1332
}
1333

1334
/* Test if a search URL matches a custom entry. If the URL has the schema "keepassxc", some special checks will be made.
1335
 * Otherwise, this simply delegates to handleURL(). */
1336
bool BrowserService::shouldIncludeEntry(Entry* entry,
1337
                                        const QString& url,
1338
                                        const QString& submitUrl,
1339
                                        const bool omitWwwSubdomain)
1340
{
1341
    // Use this special scheme to find entries by UUID
1342
    if (url.startsWith("keepassxc://by-uuid/")) {
1343
        return url.endsWith("by-uuid/" + entry->uuidToHex());
1344
    } else if (url.startsWith("keepassxc://by-path/")) {
1345
        return url.endsWith("by-path/" + entry->path());
1346
    }
1347

1348
    const auto allEntryUrls = entry->getAllUrls();
1349
    for (const auto& entryUrl : allEntryUrls) {
1350
        if (handleURL(entryUrl, url, submitUrl, omitWwwSubdomain)) {
1351
            return true;
1352
        }
1353
    }
1354

1355
    return false;
1356
}
1357

1358
#ifdef WITH_XC_BROWSER_PASSKEYS
1359
// Returns all Passkey entries for the current Relying Party
1360
QList<Entry*> BrowserService::getPasskeyEntries(const QString& rpId, const StringPairList& keyList)
1361
{
1362
    QList<Entry*> entries;
1363
    for (const auto& entry : searchEntries(rpId, "", keyList, true)) {
1364
        if (entry->hasPasskey() && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) == rpId) {
1365
            entries << entry;
1366
        }
1367
    }
1368

1369
    return entries;
1370
}
1371

1372
// Returns all Passkey entries for the current Relying Party and identical user handle
1373
QList<Entry*> BrowserService::getPasskeyEntriesWithUserHandle(const QString& rpId,
1374
                                                              const QString& userId,
1375
                                                              const StringPairList& keyList)
1376
{
1377
    QList<Entry*> entries;
1378
    for (const auto& entry : searchEntries(rpId, "", keyList, true)) {
1379
        if (entry->hasPasskey() && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_RELYING_PARTY) == rpId
1380
            && entry->attributes()->value(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE) == userId) {
1381
            entries << entry;
1382
        }
1383
    }
1384

1385
    return entries;
1386
}
1387

1388
// Get all entries for the site that are allowed by the server
1389
QList<Entry*> BrowserService::getPasskeyAllowedEntries(const QJsonObject& assertionOptions,
1390
                                                       const QString& rpId,
1391
                                                       const StringPairList& keyList)
1392
{
1393
    QList<Entry*> entries;
1394
    const auto allowedCredentials = passkeyUtils()->getAllowedCredentialsFromAssertionOptions(assertionOptions);
1395
    if (!assertionOptions["allowCredentials"].toArray().isEmpty() && allowedCredentials.isEmpty()) {
1396
        return {};
1397
    }
1398

1399
    for (const auto& entry : getPasskeyEntries(rpId, keyList)) {
1400
        // If allowedCredentials.isEmpty() check if entry contains an extra attribute for user handle.
1401
        // If that is found, the entry should be allowed.
1402
        // See: https://w3c.github.io/webauthn/#dom-authenticatorassertionresponse-userhandle
1403
        if (allowedCredentials.contains(passkeyUtils()->getCredentialIdFromEntry(entry))
1404
            || (allowedCredentials.isEmpty()
1405
                && entry->attributes()->hasKey(BrowserPasskeys::KPEX_PASSKEY_USER_HANDLE))) {
1406
            entries << entry;
1407
        }
1408
    }
1409

1410
    return entries;
1411
}
1412

1413
// Checks if the same user ID already exists for the current RP ID
1414
bool BrowserService::isPasskeyCredentialExcluded(const QJsonArray& excludeCredentials,
1415
                                                 const QString& rpId,
1416
                                                 const StringPairList& keyList)
1417
{
1418
    QStringList allIds;
1419
    for (const auto& cred : excludeCredentials) {
1420
        allIds << cred["id"].toString();
1421
    }
1422

1423
    const auto passkeyEntries = getPasskeyEntries(rpId, keyList);
1424
    return std::any_of(passkeyEntries.begin(), passkeyEntries.end(), [&](const auto& entry) {
1425
        return allIds.contains(passkeyUtils()->getCredentialIdFromEntry(entry));
1426
    });
1427
}
1428

1429
QJsonObject BrowserService::getPasskeyError(int errorCode) const
1430
{
1431
    return QJsonObject({{"errorCode", errorCode}});
1432
}
1433
#endif
1434

1435
bool BrowserService::handleURL(const QString& entryUrl,
1436
                               const QString& siteUrl,
1437
                               const QString& formUrl,
1438
                               const bool omitWwwSubdomain)
1439
{
1440
    if (entryUrl.isEmpty()) {
1441
        return false;
1442
    }
1443

1444
    QUrl entryQUrl;
1445
    if (entryUrl.contains("://")) {
1446
        entryQUrl = entryUrl;
1447
    } else {
1448
        entryQUrl = QUrl::fromUserInput(entryUrl);
1449

1450
        if (browserSettings()->matchUrlScheme()) {
1451
            entryQUrl.setScheme("https");
1452
        }
1453
    }
1454

1455
    // Remove WWW subdomain from matching if group setting is enabled
1456
    if (omitWwwSubdomain && entryQUrl.host().startsWith("www.")) {
1457
        entryQUrl.setHost(entryQUrl.host().remove("www."));
1458
    }
1459

1460
    // Make a direct compare if a local file is used
1461
    if (siteUrl.startsWith("file://")) {
1462
        return entryUrl == formUrl;
1463
    }
1464

1465
    // URL host validation fails
1466
    if (entryQUrl.host().isEmpty()) {
1467
        return false;
1468
    }
1469

1470
    // Match port, if used
1471
    QUrl siteQUrl(siteUrl);
1472
    if (entryQUrl.port() > 0 && entryQUrl.port() != siteQUrl.port()) {
1473
        return false;
1474
    }
1475

1476
    // Match scheme
1477
    if (browserSettings()->matchUrlScheme() && !entryQUrl.scheme().isEmpty()
1478
        && entryQUrl.scheme().compare(siteQUrl.scheme()) != 0) {
1479
        return false;
1480
    }
1481

1482
    // Check for illegal characters
1483
    QRegularExpression re("[<>\\^`{|}]");
1484
    if (re.match(entryUrl).hasMatch()) {
1485
        return false;
1486
    }
1487

1488
    // Match the base domain
1489
    if (urlTools()->getBaseDomainFromUrl(siteQUrl.host()) != urlTools()->getBaseDomainFromUrl(entryQUrl.host())) {
1490
        return false;
1491
    }
1492

1493
    // Match the subdomains with the limited wildcard
1494
    if (siteQUrl.host().endsWith(entryQUrl.host())) {
1495
        return true;
1496
    }
1497

1498
    return false;
1499
}
1500

1501
QSharedPointer<Database> BrowserService::getDatabase(const QUuid& rootGroupUuid)
1502
{
1503
    if (!rootGroupUuid.isNull()) {
1504
        const auto openDatabases = getOpenDatabases();
1505
        for (const auto& db : openDatabases) {
1506
            if (db->rootGroup()->uuid() == rootGroupUuid) {
1507
                return db;
1508
            }
1509
        }
1510
    }
1511

1512
    if (m_currentDatabaseWidget) {
1513
        return m_currentDatabaseWidget->database();
1514
    }
1515
    return {};
1516
}
1517

1518
QList<QSharedPointer<Database>> BrowserService::getOpenDatabases()
1519
{
1520
    QList<QSharedPointer<Database>> databaseList;
1521
    for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
1522
        if (!dbWidget->isLocked()) {
1523
            databaseList << dbWidget->database();
1524
        }
1525
    }
1526
    return databaseList;
1527
}
1528

1529
QSharedPointer<Database> BrowserService::selectedDatabase()
1530
{
1531
    QList<DatabaseWidget*> databaseWidgets;
1532
    for (auto dbWidget : getMainWindow()->getOpenDatabases()) {
1533
        // Add only open databases
1534
        if (!dbWidget->isLocked()) {
1535
            databaseWidgets << dbWidget;
1536
        }
1537
    }
1538

1539
    BrowserEntrySaveDialog browserEntrySaveDialog(m_currentDatabaseWidget);
1540
    int openDatabaseCount = browserEntrySaveDialog.setItems(databaseWidgets, m_currentDatabaseWidget);
1541
    if (openDatabaseCount > 1) {
1542
        int res = browserEntrySaveDialog.exec();
1543
        if (res == QDialog::Accepted) {
1544
            const auto selectedDatabase = browserEntrySaveDialog.getSelected();
1545
            if (selectedDatabase.length() > 0) {
1546
                int index = selectedDatabase[0]->data(Qt::UserRole).toInt();
1547
                return databaseWidgets[index]->database();
1548
            }
1549
        } else {
1550
            return {};
1551
        }
1552
    }
1553

1554
    // Return current database
1555
    return getDatabase();
1556
}
1557

1558
void BrowserService::hideWindow() const
1559
{
1560
    if (m_prevWindowState == WindowState::Minimized) {
1561
        getMainWindow()->showMinimized();
1562
    } else {
1563
#ifdef Q_OS_MACOS
1564
        if (m_prevWindowState == WindowState::Hidden) {
1565
            macUtils()->hideOwnWindow();
1566
        } else {
1567
            macUtils()->raiseLastActiveWindow();
1568
        }
1569
#else
1570
        if (m_prevWindowState == WindowState::Hidden) {
1571
            getMainWindow()->hideWindow();
1572
        } else {
1573
            getMainWindow()->lower();
1574
        }
1575
#endif
1576
    }
1577
}
1578

1579
void BrowserService::raiseWindow(const bool force)
1580
{
1581
    m_prevWindowState = WindowState::Normal;
1582
    if (getMainWindow()->isMinimized()) {
1583
        m_prevWindowState = WindowState::Minimized;
1584
    }
1585
#ifdef Q_OS_MACOS
1586
    Q_UNUSED(force)
1587

1588
    if (macUtils()->isHidden()) {
1589
        m_prevWindowState = WindowState::Hidden;
1590
    }
1591
    macUtils()->raiseOwnWindow();
1592
    Tools::wait(500);
1593
#else
1594
    if (getMainWindow()->isHidden()) {
1595
        m_prevWindowState = WindowState::Hidden;
1596
    }
1597

1598
    if (force) {
1599
        getMainWindow()->bringToFront();
1600
    }
1601
#endif
1602
}
1603

1604
void BrowserService::updateWindowState()
1605
{
1606
    m_prevWindowState = WindowState::Normal;
1607
    if (getMainWindow()->isMinimized()) {
1608
        m_prevWindowState = WindowState::Minimized;
1609
    }
1610
#ifdef Q_OS_MACOS
1611
    if (macUtils()->isHidden()) {
1612
        m_prevWindowState = WindowState::Hidden;
1613
    }
1614
#else
1615
    if (getMainWindow()->isHidden()) {
1616
        m_prevWindowState = WindowState::Hidden;
1617
    }
1618
#endif
1619
}
1620

1621
void BrowserService::databaseLocked(DatabaseWidget* dbWidget)
1622
{
1623
    if (dbWidget) {
1624
        QJsonObject msg;
1625
        msg["action"] = QString("database-locked");
1626
        m_browserHost->broadcastClientMessage(msg);
1627
    }
1628
}
1629

1630
void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget)
1631
{
1632
    if (dbWidget) {
1633
        if (m_bringToFrontRequested) {
1634
            m_bringToFrontRequested = false;
1635
            hideWindow();
1636
        }
1637

1638
        QJsonObject msg;
1639
        msg["action"] = QString("database-unlocked");
1640
        m_browserHost->broadcastClientMessage(msg);
1641
    }
1642
}
1643

1644
void BrowserService::activeDatabaseChanged(DatabaseWidget* dbWidget)
1645
{
1646
    if (dbWidget) {
1647
        if (dbWidget->isLocked()) {
1648
            databaseLocked(dbWidget);
1649
        } else {
1650
            databaseUnlocked(dbWidget);
1651
        }
1652
    }
1653

1654
    m_currentDatabaseWidget = dbWidget;
1655
}
1656

1657
void BrowserService::processClientMessage(QLocalSocket* socket, const QJsonObject& message)
1658
{
1659
    auto clientID = message["clientID"].toString();
1660
    if (clientID.isEmpty()) {
1661
        return;
1662
    }
1663

1664
    // Create a new client action if we haven't seen this id yet
1665
    if (!m_browserClients.contains(clientID)) {
1666
        m_browserClients.insert(clientID, QSharedPointer<BrowserAction>::create());
1667
    }
1668

1669
    auto& action = m_browserClients.value(clientID);
1670
    auto response = action->processClientMessage(socket, message);
1671
    m_browserHost->sendClientMessage(socket, response);
1672
}
1673

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

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

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

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