From 0ce95b4c4a683f83481957c6c2506732a9b5778b Mon Sep 17 00:00:00 2001 From: "Maximilian-F. Helm" Date: Tue, 10 Feb 2026 15:43:04 +0100 Subject: [PATCH] Add option to enter a otpauth:// URI in the TotpSetupDialog --- share/translations/keepassxc_de.ts | 6 +- share/translations/keepassxc_en.ts | 12 ++ share/translations/keepassxc_en_GB.ts | 14 +- share/translations/keepassxc_en_US.ts | 14 +- src/core/Totp.cpp | 4 +- src/core/Totp.h | 3 + src/gui/TotpSetupDialog.cpp | 259 ++++++++++++++++++++------ src/gui/TotpSetupDialog.h | 8 + src/gui/TotpSetupDialog.ui | 27 ++- 9 files changed, 274 insertions(+), 73 deletions(-) diff --git a/share/translations/keepassxc_de.ts b/share/translations/keepassxc_de.ts index ec732774c..85494989c 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 f974db170..4798d94de 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 8c878dc04..bfbb31f09 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 75fcf9e92..842ab36fc 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 ed15a9fb8..7ff88c7bf 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 da857aef2..d52c8461a 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 e7e0bd749..34869dba9 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,71 +37,13 @@ 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() -{ - // 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; - } - - QString encShortName; - uint digits = Totp::DEFAULT_DIGITS; - uint step = Totp::DEFAULT_STEP; - Totp::Algorithm algorithm = Totp::DEFAULT_ALGORITHM; - Totp::StorageFormat format = Totp::DEFAULT_FORMAT; - - if (m_ui->radioSteam->isChecked()) { - digits = Totp::STEAM_DIGITS; - encShortName = Totp::STEAM_SHORTNAME; - } else if (m_ui->radioCustom->isChecked()) { - algorithm = static_cast(m_ui->algorithmComboBox->currentData().toInt()); - step = m_ui->stepSpinBox->value(); - digits = m_ui->digitsSpinBox->value(); - } - - auto settings = m_entry->totpSettings(); - if (settings) { - if (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?"), - MessageBox::Delete | MessageBox::Cancel); - if (answer != MessageBox::Delete) { - 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)); - emit totpUpdated(); - close(); -} - -void TotpSetupDialog::toggleCustom(bool status) -{ - m_ui->customSettingsGroup->setEnabled(status); -} - void TotpSetupDialog::init() { // Add algorithm choices @@ -133,3 +77,200 @@ void TotpSetupDialog::init() 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()) { + newSettings = createFromCustom(); + } + + if (newSettings.isNull()) { + return; + } + + auto settings = m_entry->totpSettings(); + if (settings) { + 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?"), + MessageBox::Delete | MessageBox::Cancel); + if (answer != MessageBox::Delete) { + return; + } + } + } + + m_entry->setTotp(newSettings); + emit totpUpdated(); + close(); +} + +void TotpSetupDialog::toggleCustom(bool status) +{ + m_ui->customSettingsGroup->setEnabled(status); +} + +void TotpSetupDialog::toggleUri(bool status) +{ + if (status) { + m_ui->labelSecretKey->setText(tr("URI:")); + } else { + m_ui->labelSecretKey->setText(tr("Secret Key:")); + } +} + +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; + + auto settings = m_entry->totpSettings(); + if (settings) { + format = settings->format; + } + + 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; + } + } + + 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 8b88cb8e0..857d10a7e 100644 --- a/src/gui/TotpSetupDialog.h +++ b/src/gui/TotpSetupDialog.h @@ -43,11 +43,19 @@ signals: 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 f8c95f4c4..c1336c207 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