diff --git a/share/translations/keepassxc_de.ts b/share/translations/keepassxc_de.ts index ec732774ca..85494989c4 100644 --- a/share/translations/keepassxc_de.ts +++ b/share/translations/keepassxc_de.ts @@ -10422,6 +10422,10 @@ Example: JBSWY3DPEHPK3PXP Sie haben einen ungültigen geheimen Schlüssel angegeben. Der Schlüssel muss im Base32-Format sein. Beispiel: JBSWY3DPEHPK3PXP + + You have entered an invalid TOTP URI. The URI must start with otpauth://totp/ + Sie haben eine ungültige Totp Uri eingegeben. Die Uri muss mit otpauth://totp/ beginnen. + Confirm Remove TOTP Settings Löschen der TOTP-Einstellungen bestätigen @@ -10671,4 +10675,4 @@ Beispiel: JBSWY3DPEHPK3PXP Unbekannt - \ No newline at end of file + diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index f974db170b..4798d94de9 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -10440,6 +10440,10 @@ This option is deprecated, use --set-key-file instead. Secret Key: + + URI: + + Secret key must be in Base32 format @@ -10452,6 +10456,10 @@ This option is deprecated, use --set-key-file instead. Default settings (RFC 6238) + + Totp Uri + + Steam® settings @@ -10498,6 +10506,10 @@ This option is deprecated, use --set-key-file instead. Example: JBSWY3DPEHPK3PXP + + You have entered an invalid TOTP URI. The URI must start with otpauth://totp/ + + Confirm Remove TOTP Settings diff --git a/share/translations/keepassxc_en_GB.ts b/share/translations/keepassxc_en_GB.ts index 8c878dc04f..bfbb31f09b 100644 --- a/share/translations/keepassxc_en_GB.ts +++ b/share/translations/keepassxc_en_GB.ts @@ -10363,6 +10363,10 @@ This option is deprecated, use --set-key-file instead. Secret Key: Secret Key: + + URI: + URI: + Secret key must be in Base32 format Secret key must be in Base32 format @@ -10375,6 +10379,10 @@ This option is deprecated, use --set-key-file instead. Default settings (RFC 6238) Default settings (RFC 6238) + + Totp Uri + Totp Uri + Steam® settings Steam® settings @@ -10422,6 +10430,10 @@ Example: JBSWY3DPEHPK3PXP You have entered an invalid secret key. The key must be in Base32 format. Example: JBSWY3DPEHPK3PXP + + You have entered an invalid TOTP URI. The URI must start with otpauth://totp/ + You have entered an invalid Totp Uri. The Uri must start with otpauth://totp/ + Confirm Remove TOTP Settings Confirm Remove TOTP Settings @@ -10671,4 +10683,4 @@ Example: JBSWY3DPEHPK3PXP Unknown - \ No newline at end of file + diff --git a/share/translations/keepassxc_en_US.ts b/share/translations/keepassxc_en_US.ts index 75fcf9e921..842ab36fcc 100644 --- a/share/translations/keepassxc_en_US.ts +++ b/share/translations/keepassxc_en_US.ts @@ -10363,6 +10363,10 @@ This option is deprecated, use --set-key-file instead. Secret Key: Secret Key: + + URI: + URI: + Secret key must be in Base32 format Secret key must be in Base32 format @@ -10375,6 +10379,10 @@ This option is deprecated, use --set-key-file instead. Default settings (RFC 6238) Default settings (RFC 6238) + + Totp Uri + Totp Uri + Steam® settings Steam® settings @@ -10422,6 +10430,10 @@ Example: JBSWY3DPEHPK3PXP You have entered an invalid secret key. The key must be in Base32 format. Example: JBSWY3DPEHPK3PXP + + You have entered an invalid TOTP URI. The URI must start with otpauth://totp/ + You have entered an invalid Totp Uri. The Uri must start with otpauth://totp/ + Confirm Remove TOTP Settings Confirm Remove TOTP Settings @@ -10671,4 +10683,4 @@ Example: JBSWY3DPEHPK3PXP Unknown - \ No newline at end of file + diff --git a/src/core/Totp.cpp b/src/core/Totp.cpp index ed15a9fb86..7ff88c7bf0 100644 --- a/src/core/Totp.cpp +++ b/src/core/Totp.cpp @@ -34,7 +34,7 @@ static QList totpEncoders{ {"steam", Totp::STEAM_SHORTNAME, "23456789BCDFGHJKMNPQRTVWXY", Totp::STEAM_DIGITS, Totp::DEFAULT_STEP, true}, }; -static Totp::Algorithm getHashTypeByName(const QString& name) +Totp::Algorithm Totp::getHashTypeByName(const QString& name) { auto nameUpper = name.toUpper(); if (nameUpper == "SHA512" || nameUpper == "HMAC-SHA-512") { @@ -46,7 +46,7 @@ static Totp::Algorithm getHashTypeByName(const QString& name) return Totp::Algorithm::Sha1; } -static QString getNameForHashType(const Totp::Algorithm hashType) +QString Totp::getNameForHashType(const Totp::Algorithm hashType) { switch (hashType) { case Totp::Algorithm::Sha512: diff --git a/src/core/Totp.h b/src/core/Totp.h index da857aef2b..d52c8461ad 100644 --- a/src/core/Totp.h +++ b/src/core/Totp.h @@ -98,6 +98,9 @@ namespace Totp bool hasCustomSettings(const QSharedPointer& settings); + Totp::Algorithm getHashTypeByName(const QString& name); + QString getNameForHashType(const Totp::Algorithm hashType); + QList> supportedEncoders(); QList> supportedAlgorithms(); diff --git a/src/gui/TotpSetupDialog.cpp b/src/gui/TotpSetupDialog.cpp index e7e0bd7498..34869dba9b 100644 --- a/src/gui/TotpSetupDialog.cpp +++ b/src/gui/TotpSetupDialog.cpp @@ -22,6 +22,8 @@ #include "core/Totp.h" #include "gui/MessageBox.h" +#include + TotpSetupDialog::TotpSetupDialog(QWidget* parent, Entry* entry) : QDialog(parent) , m_ui(new Ui::TotpSetupDialog()) @@ -35,45 +37,67 @@ TotpSetupDialog::TotpSetupDialog(QWidget* parent, Entry* entry) connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close())); connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(saveSettings())); connect(m_ui->radioCustom, SIGNAL(toggled(bool)), SLOT(toggleCustom(bool))); + connect(m_ui->radioUri, SIGNAL(toggled(bool)), SLOT(toggleUri(bool))); init(); } TotpSetupDialog::~TotpSetupDialog() = default; -void TotpSetupDialog::saveSettings() +void TotpSetupDialog::init() { - // Secret key sanity check - // Convert user input to all uppercase and remove '=' - auto key = m_ui->seedEdit->text().toUpper().remove(" ").remove("=").trimmed().toLatin1(); - auto sanitizedKey = Base32::sanitizeInput(key); - // Use startsWith to ignore added '=' for padding at the end - if (!sanitizedKey.startsWith(key)) { - MessageBox::information(this, - tr("Invalid TOTP Secret"), - tr("You have entered an invalid secret key. The key must be in Base32 format.\n" - "Example: JBSWY3DPEHPK3PXP")); - return; + // Add algorithm choices + auto algorithms = Totp::supportedAlgorithms(); + for (const auto& item : algorithms) { + m_ui->algorithmComboBox->addItem(item.first, item.second); } + m_ui->algorithmComboBox->setCurrentIndex(0); + m_ui->invalidKeyLabel->setVisible(false); - QString encShortName; - uint digits = Totp::DEFAULT_DIGITS; - uint step = Totp::DEFAULT_STEP; - Totp::Algorithm algorithm = Totp::DEFAULT_ALGORITHM; - Totp::StorageFormat format = Totp::DEFAULT_FORMAT; + // Read entry totp settings + auto settings = m_entry->totpSettings(); + if (settings) { + auto key = settings->key; + m_ui->seedEdit->setText(key.remove("=")); + m_ui->seedEdit->setCursorPosition(0); + m_ui->stepSpinBox->setValue(settings->step); - if (m_ui->radioSteam->isChecked()) { - digits = Totp::STEAM_DIGITS; - encShortName = Totp::STEAM_SHORTNAME; + if (settings->encoder.shortName == Totp::STEAM_SHORTNAME) { + m_ui->radioSteam->setChecked(true); + } else if (Totp::hasCustomSettings(settings)) { + m_ui->radioCustom->setChecked(true); + m_ui->digitsSpinBox->setValue(settings->digits); + int index = m_ui->algorithmComboBox->findData(settings->algorithm); + if (index != -1) { + m_ui->algorithmComboBox->setCurrentIndex(index); + } + } + + auto error = Totp::checkValidSettings(settings); + m_ui->invalidKeyLabel->setVisible(!error.isEmpty()); + } +} + +void TotpSetupDialog::saveSettings() +{ + QSharedPointer newSettings; + if (m_ui->radioDefault->isChecked()) { + newSettings = createFromRfc6238(); + } else if (m_ui->radioUri->isChecked()) { + newSettings = createFromUri(); + } else if (m_ui->radioSteam->isChecked()) { + newSettings = createFromSteam(); } else if (m_ui->radioCustom->isChecked()) { - algorithm = static_cast(m_ui->algorithmComboBox->currentData().toInt()); - step = m_ui->stepSpinBox->value(); - digits = m_ui->digitsSpinBox->value(); + newSettings = createFromCustom(); + } + + if (newSettings.isNull()) { + return; } auto settings = m_entry->totpSettings(); if (settings) { - if (key.isEmpty()) { + if (newSettings->key.isEmpty()) { auto answer = MessageBox::question(this, tr("Confirm Remove TOTP Settings"), tr("Are you sure you want to delete TOTP settings for this entry?"), @@ -82,15 +106,9 @@ void TotpSetupDialog::saveSettings() return; } } - - format = settings->format; - if (format == Totp::StorageFormat::LEGACY && m_ui->radioCustom->isChecked()) { - // Implicitly upgrade to the OTPURL format to allow for custom settings - format = Totp::DEFAULT_FORMAT; - } } - m_entry->setTotp(Totp::createSettings(key, digits, step, format, encShortName, algorithm)); + m_entry->setTotp(newSettings); emit totpUpdated(); close(); } @@ -100,36 +118,159 @@ void TotpSetupDialog::toggleCustom(bool status) m_ui->customSettingsGroup->setEnabled(status); } -void TotpSetupDialog::init() +void TotpSetupDialog::toggleUri(bool status) { - // Add algorithm choices - auto algorithms = Totp::supportedAlgorithms(); - for (const auto& item : algorithms) { - m_ui->algorithmComboBox->addItem(item.first, item.second); + if (status) { + m_ui->labelSecretKey->setText(tr("URI:")); + } else { + m_ui->labelSecretKey->setText(tr("Secret Key:")); } - m_ui->algorithmComboBox->setCurrentIndex(0); - m_ui->invalidKeyLabel->setVisible(false); +} + +QSharedPointer TotpSetupDialog::createFromRfc6238() +{ + QString key = sanitizeSecretKey(); + if (key == QStringLiteral("err")) { + MessageBox::information(this, + tr("Invalid TOTP Secret"), + tr("You have entered an invalid secret key. The key must be in Base32 format.\n" + "Example: JBSWY3DPEHPK3PXP")); + return nullptr; + } + + QString encShortName; + uint digits = Totp::DEFAULT_DIGITS; + uint step = Totp::DEFAULT_STEP; + Totp::Algorithm algorithm = Totp::DEFAULT_ALGORITHM; + Totp::StorageFormat format = Totp::DEFAULT_FORMAT; - // Read entry totp settings auto settings = m_entry->totpSettings(); if (settings) { - auto key = settings->key; - m_ui->seedEdit->setText(key.remove("=")); - m_ui->seedEdit->setCursorPosition(0); - m_ui->stepSpinBox->setValue(settings->step); + format = settings->format; + } - if (settings->encoder.shortName == Totp::STEAM_SHORTNAME) { - m_ui->radioSteam->setChecked(true); - } else if (Totp::hasCustomSettings(settings)) { - m_ui->radioCustom->setChecked(true); - m_ui->digitsSpinBox->setValue(settings->digits); - int index = m_ui->algorithmComboBox->findData(settings->algorithm); - if (index != -1) { - m_ui->algorithmComboBox->setCurrentIndex(index); - } + return Totp::createSettings(key, digits, step, format, encShortName, algorithm); +} + +QSharedPointer TotpSetupDialog::createFromUri() +{ + auto uri = QUrl(m_ui->seedEdit->text()); + if (!uri.isValid() || uri.scheme() != "otpauth") { + MessageBox::information(this, + tr("Invalid TOTP Secret"), + tr("You have entered an invalid TOTP URI. The URI must start with otpauth://totp/")); + return nullptr; + } + + QString encShortName; + uint digits = Totp::DEFAULT_DIGITS; + uint step = Totp::DEFAULT_STEP; + Totp::Algorithm algorithm = Totp::DEFAULT_ALGORITHM; + Totp::StorageFormat format = Totp::DEFAULT_FORMAT; + + QUrlQuery query(uri); + + if (!query.hasQueryItem("secret")) { + MessageBox::information(this, + tr("Invalid TOTP Secret"), + tr("You have entered an invalid TOTP URI. The URI must start with otpauth://totp/")); + return nullptr; + } + QString key = sanitizeSecretKey(query.queryItemValue("secret")); + if (key == QStringLiteral("err")) { + MessageBox::information(this, + tr("Invalid TOTP Secret"), + tr("You have entered an invalid TOTP URI. The URI must start with otpauth://totp/")); + return nullptr; + } + + if (query.hasQueryItem("digits")) { + digits = query.queryItemValue("digits").toUInt(); + } + if (query.hasQueryItem("period")) { + step = query.queryItemValue("period").toUInt(); + } + if (query.hasQueryItem("algorithm")) { + algorithm = Totp::getHashTypeByName(query.queryItemValue("algorithm")); + } + + auto settings = m_entry->totpSettings(); + if (settings) { + format = settings->format; + } + + return Totp::createSettings(key, digits, step, format, encShortName, algorithm); +} + +QSharedPointer TotpSetupDialog::createFromSteam() +{ + QString key = sanitizeSecretKey(); + if (key == QStringLiteral("err")) { + MessageBox::information(this, + tr("Invalid TOTP Secret"), + tr("You have entered an invalid secret key. The key must be in Base32 format.\n" + "Example: JBSWY3DPEHPK3PXP")); + return nullptr; + } + + QString encShortName = Totp::STEAM_SHORTNAME; + uint digits = Totp::STEAM_DIGITS; + uint step = Totp::DEFAULT_STEP; + Totp::Algorithm algorithm = Totp::DEFAULT_ALGORITHM; + Totp::StorageFormat format = Totp::DEFAULT_FORMAT; + + auto settings = m_entry->totpSettings(); + if (settings) { + format = settings->format; + } + + return Totp::createSettings(key, digits, step, format, encShortName, algorithm); +} + +QSharedPointer TotpSetupDialog::createFromCustom() +{ + QString key = sanitizeSecretKey(); + if (key == QStringLiteral("err")) { + MessageBox::information(this, + tr("Invalid TOTP Secret"), + tr("You have entered an invalid secret key. The key must be in Base32 format.\n" + "Example: JBSWY3DPEHPK3PXP")); + return nullptr; + } + + QString encShortName; + uint digits = m_ui->digitsSpinBox->value(); + uint step = m_ui->stepSpinBox->value(); + Totp::Algorithm algorithm = static_cast(m_ui->algorithmComboBox->currentData().toInt()); + Totp::StorageFormat format = Totp::DEFAULT_FORMAT; + + auto settings = m_entry->totpSettings(); + if (settings) { + format = settings->format; + if (format == Totp::StorageFormat::LEGACY) { + // Implicitly upgrade to the OTPURL format to allow for custom settings + format = Totp::DEFAULT_FORMAT; } + } - auto error = Totp::checkValidSettings(settings); - m_ui->invalidKeyLabel->setVisible(!error.isEmpty()); + return Totp::createSettings(key, digits, step, format, encShortName, algorithm); +} + +QString TotpSetupDialog::sanitizeSecretKey() +{ + return sanitizeSecretKey(m_ui->seedEdit->text()); +} + +QString TotpSetupDialog::sanitizeSecretKey(const QString& key) +{ + // Secret key sanity check + // Convert user input to all uppercase and remove '=' + auto keyCleaned = key.toUpper().remove(" ").remove("=").trimmed(); + auto keyBytes = keyCleaned.toLatin1(); + auto sanitizedKey = Base32::sanitizeInput(keyBytes); + // Use startsWith to ignore added '=' for padding at the end + if (!sanitizedKey.startsWith(keyBytes)) { + return QStringLiteral("err"); } + return sanitizedKey; } diff --git a/src/gui/TotpSetupDialog.h b/src/gui/TotpSetupDialog.h index 8b88cb8e0e..857d10a7ec 100644 --- a/src/gui/TotpSetupDialog.h +++ b/src/gui/TotpSetupDialog.h @@ -43,11 +43,19 @@ class TotpSetupDialog : public QDialog private slots: void toggleCustom(bool status); + void toggleUri(bool status); void saveSettings(); private: QScopedPointer m_ui; Entry* m_entry; + + QSharedPointer createFromRfc6238(); + QSharedPointer createFromUri(); + QSharedPointer createFromSteam(); + QSharedPointer createFromCustom(); + QString sanitizeSecretKey(); + QString sanitizeSecretKey(const QString& key); }; #endif // KEEPASSX_SETUPTOTPDIALOG_H diff --git a/src/gui/TotpSetupDialog.ui b/src/gui/TotpSetupDialog.ui index f8c95f4c45..c1336c207b 100644 --- a/src/gui/TotpSetupDialog.ui +++ b/src/gui/TotpSetupDialog.ui @@ -7,7 +7,7 @@ 0 0 249 - 278 + 307 @@ -18,7 +18,6 @@ - 75 true @@ -26,7 +25,7 @@ Error: secret key is invalid - Qt::AlignCenter + Qt::AlignmentFlag::AlignCenter @@ -39,7 +38,7 @@ 5 - + Secret Key: @@ -97,6 +96,16 @@ + + + + Totp Uri + + + settingsButtonGroup + + + @@ -130,13 +139,13 @@ - QFormLayout::ExpandingFieldsGrow + QFormLayout::FieldGrowthPolicy::ExpandingFieldsGrow - QFormLayout::DontWrapRows + QFormLayout::RowWrapPolicy::DontWrapRows - Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing + Qt::AlignmentFlag::AlignRight|Qt::AlignmentFlag::AlignTop|Qt::AlignmentFlag::AlignTrailing 7 @@ -215,10 +224,10 @@ - Qt::Horizontal + Qt::Orientation::Horizontal - QDialogButtonBox::Cancel|QDialogButtonBox::Ok + QDialogButtonBox::StandardButton::Cancel|QDialogButtonBox::StandardButton::Ok