Correct issues with TOTP Setup

* Fix #3142 - Warn user when entering invalid TOTP secret key.
* Fix #773 - The TOTP dialog now listens for the copy shortcut without having to press the Copy button.

* Add ability to choose hash algorithm from the TOTP setup dialog
* Add upgrade to "otp" attribute when custom attributes are chosen to prevent data loss

Ran make format
This commit is contained in:
Jonathan White 2019-10-14 09:25:45 -04:00
parent 71085838db
commit 99a2d66086
9 changed files with 211 additions and 122 deletions

View File

@ -442,16 +442,16 @@ QString Entry::totp() const
void Entry::setTotp(QSharedPointer<Totp::Settings> settings) void Entry::setTotp(QSharedPointer<Totp::Settings> settings)
{ {
beginUpdate(); beginUpdate();
if (settings->key.isEmpty()) {
m_data.totpSettings.reset();
m_attributes->remove(Totp::ATTRIBUTE_OTP); m_attributes->remove(Totp::ATTRIBUTE_OTP);
m_attributes->remove(Totp::ATTRIBUTE_SEED); m_attributes->remove(Totp::ATTRIBUTE_SEED);
m_attributes->remove(Totp::ATTRIBUTE_SETTINGS); m_attributes->remove(Totp::ATTRIBUTE_SETTINGS);
if (settings->key.isEmpty()) {
m_data.totpSettings.reset();
} else { } else {
m_data.totpSettings = std::move(settings); m_data.totpSettings = std::move(settings);
auto text = Totp::writeSettings(m_data.totpSettings, title(), username()); auto text = Totp::writeSettings(m_data.totpSettings, title(), username());
if (m_attributes->hasKey(Totp::ATTRIBUTE_OTP)) { if (m_data.totpSettings->format != Totp::StorageFormat::LEGACY) {
m_attributes->set(Totp::ATTRIBUTE_OTP, text, true); m_attributes->set(Totp::ATTRIBUTE_OTP, text, true);
} else { } else {
m_attributes->set(Totp::ATTRIBUTE_SEED, m_data.totpSettings->key, true); m_attributes->set(Totp::ATTRIBUTE_SEED, m_data.totpSettings->key, true);

View File

@ -22,6 +22,9 @@
#include "core/Clock.h" #include "core/Clock.h"
#include "core/Config.h" #include "core/Config.h"
#include "gui/Clipboard.h" #include "gui/Clipboard.h"
#include "gui/MainWindow.h"
#include <QShortcut>
TotpDialog::TotpDialog(QWidget* parent, Entry* entry) TotpDialog::TotpDialog(QWidget* parent, Entry* entry)
: QDialog(parent) : QDialog(parent)
@ -39,6 +42,7 @@ TotpDialog::TotpDialog(QWidget* parent, Entry* entry)
resetCounter(); resetCounter();
updateProgressBar(); updateProgressBar();
connect(parent, SIGNAL(lockedDatabase()), SLOT(close()));
connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateProgressBar())); connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateProgressBar()));
connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateSeconds())); connect(&m_totpUpdateTimer, SIGNAL(timeout()), this, SLOT(updateSeconds()));
m_totpUpdateTimer.start(m_step * 10); m_totpUpdateTimer.start(m_step * 10);
@ -46,6 +50,8 @@ TotpDialog::TotpDialog(QWidget* parent, Entry* entry)
setAttribute(Qt::WA_DeleteOnClose); setAttribute(Qt::WA_DeleteOnClose);
new QShortcut(QKeySequence(QKeySequence::Copy), this, SLOT(copyToClipboard()));
m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Copy")); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Copy"));
connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close())); connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close()));
@ -61,9 +67,9 @@ void TotpDialog::copyToClipboard()
clipboard()->setText(m_entry->totp()); clipboard()->setText(m_entry->totp());
if (config()->get("HideWindowOnCopy").toBool()) { if (config()->get("HideWindowOnCopy").toBool()) {
if (config()->get("MinimizeOnCopy").toBool()) { if (config()->get("MinimizeOnCopy").toBool()) {
qobject_cast<DatabaseWidget*>(parent())->window()->showMinimized(); getMainWindow()->showMinimized();
} else if (config()->get("DropToBackgroundOnCopy").toBool()) { } else if (config()->get("DropToBackgroundOnCopy").toBool()) {
qobject_cast<DatabaseWidget*>(parent())->window()->lower(); getMainWindow()->lower();
window()->lower(); window()->lower();
} }
} }

View File

@ -16,10 +16,12 @@
*/ */
#include "TotpExportSettingsDialog.h" #include "TotpExportSettingsDialog.h"
#include "core/Config.h" #include "core/Config.h"
#include "core/Entry.h" #include "core/Entry.h"
#include "gui/Clipboard.h" #include "gui/Clipboard.h"
#include "gui/DatabaseWidget.h" #include "gui/DatabaseWidget.h"
#include "gui/MainWindow.h"
#include "gui/SquareSvgWidget.h" #include "gui/SquareSvgWidget.h"
#include "qrcode/QrCode.h" #include "qrcode/QrCode.h"
#include "totp/totp.h" #include "totp/totp.h"
@ -29,6 +31,7 @@
#include <QLabel> #include <QLabel>
#include <QMessageBox> #include <QMessageBox>
#include <QPushButton> #include <QPushButton>
#include <QShortcut>
#include <QSizePolicy> #include <QSizePolicy>
#include <QVBoxLayout> #include <QVBoxLayout>
@ -55,10 +58,13 @@ TotpExportSettingsDialog::TotpExportSettingsDialog(DatabaseWidget* parent, Entry
connect(m_buttonBox, SIGNAL(rejected()), SLOT(close())); connect(m_buttonBox, SIGNAL(rejected()), SLOT(close()));
connect(m_buttonBox, SIGNAL(accepted()), SLOT(copyToClipboard())); connect(m_buttonBox, SIGNAL(accepted()), SLOT(copyToClipboard()));
connect(m_timer, SIGNAL(timeout()), this, SLOT(autoClose())); connect(m_timer, SIGNAL(timeout()), SLOT(autoClose()));
connect(parent, SIGNAL(lockedDatabase()), this, SLOT(close())); connect(parent, SIGNAL(lockedDatabase()), SLOT(close()));
new QShortcut(QKeySequence(QKeySequence::Copy), this, SLOT(copyToClipboard()));
m_buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Copy")); m_buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Copy"));
m_buttonBox->setFocus();
m_countDown->setAlignment(Qt::AlignCenter); m_countDown->setAlignment(Qt::AlignCenter);
m_secTillClose = 45; m_secTillClose = 45;
@ -92,8 +98,6 @@ TotpExportSettingsDialog::TotpExportSettingsDialog(DatabaseWidget* parent, Entry
errorBox->exec(); errorBox->exec();
close(); close();
} }
show();
} }
void TotpExportSettingsDialog::copyToClipboard() void TotpExportSettingsDialog::copyToClipboard()
@ -101,9 +105,9 @@ void TotpExportSettingsDialog::copyToClipboard()
clipboard()->setText(m_totpUri); clipboard()->setText(m_totpUri);
if (config()->get("HideWindowOnCopy").toBool()) { if (config()->get("HideWindowOnCopy").toBool()) {
if (config()->get("MinimizeOnCopy").toBool()) { if (config()->get("MinimizeOnCopy").toBool()) {
static_cast<DatabaseWidget*>(parent())->window()->showMinimized(); getMainWindow()->showMinimized();
} else if (config()->get("DropToBackgroundOnCopy").toBool()) { } else if (config()->get("DropToBackgroundOnCopy").toBool()) {
static_cast<DatabaseWidget*>(parent())->window()->lower(); getMainWindow()->lower();
window()->lower(); window()->lower();
} }
} }

View File

@ -19,6 +19,8 @@
#include "TotpSetupDialog.h" #include "TotpSetupDialog.h"
#include "ui_TotpSetupDialog.h" #include "ui_TotpSetupDialog.h"
#include "core/Base32.h"
#include "gui/MessageBox.h"
#include "totp/totp.h" #include "totp/totp.h"
TotpSetupDialog::TotpSetupDialog(QWidget* parent, Entry* entry) TotpSetupDialog::TotpSetupDialog(QWidget* parent, Entry* entry)
@ -43,38 +45,73 @@ TotpSetupDialog::~TotpSetupDialog()
void TotpSetupDialog::saveSettings() void TotpSetupDialog::saveSettings()
{ {
// Secret key sanity check
auto key = m_ui->seedEdit->text().toLatin1();
auto sanitizedKey = Base32::sanitizeInput(key);
if (sanitizedKey != 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; QString encShortName;
uint digits = Totp::DEFAULT_DIGITS; uint digits = Totp::DEFAULT_DIGITS;
uint step = Totp::DEFAULT_STEP; uint step = Totp::DEFAULT_STEP;
Totp::Algorithm algorithm = Totp::DEFAULT_ALGORITHM;
Totp::StorageFormat format = Totp::DEFAULT_FORMAT;
if (m_ui->radioSteam->isChecked()) { if (m_ui->radioSteam->isChecked()) {
digits = Totp::STEAM_DIGITS; digits = Totp::STEAM_DIGITS;
encShortName = Totp::STEAM_SHORTNAME; encShortName = Totp::STEAM_SHORTNAME;
} else if (m_ui->radioCustom->isChecked()) { } else if (m_ui->radioCustom->isChecked()) {
algorithm = static_cast<Totp::Algorithm>(m_ui->algorithmComboBox->currentData().toInt());
step = m_ui->stepSpinBox->value(); step = m_ui->stepSpinBox->value();
if (m_ui->radio8Digits->isChecked()) { digits = m_ui->digitsSpinBox->value();
digits = 8; }
} else if (m_ui->radio7Digits->isChecked()) {
digits = 7; 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;
} }
} }
auto settings = Totp::createSettings( format = settings->format;
m_ui->seedEdit->text(), digits, step, encShortName, Totp::HashType::Sha1, m_entry->totpSettings()); if (format == Totp::StorageFormat::LEGACY && m_ui->radioCustom->isChecked()) {
m_entry->setTotp(settings); // 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(); emit totpUpdated();
close(); close();
} }
void TotpSetupDialog::toggleCustom(bool status) void TotpSetupDialog::toggleCustom(bool status)
{ {
m_ui->customGroup->setEnabled(status); m_ui->customSettingsGroup->setEnabled(status);
} }
void TotpSetupDialog::init() void TotpSetupDialog::init()
{ {
// 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);
// Read entry totp settings
auto settings = m_entry->totpSettings(); auto settings = m_entry->totpSettings();
if (!settings.isNull()) { if (settings) {
m_ui->seedEdit->setText(settings->key); m_ui->seedEdit->setText(settings->key);
m_ui->stepSpinBox->setValue(settings->step); m_ui->stepSpinBox->setValue(settings->step);
@ -82,12 +119,10 @@ void TotpSetupDialog::init()
m_ui->radioSteam->setChecked(true); m_ui->radioSteam->setChecked(true);
} else if (settings->custom) { } else if (settings->custom) {
m_ui->radioCustom->setChecked(true); m_ui->radioCustom->setChecked(true);
if (settings->digits == 8) { m_ui->digitsSpinBox->setValue(settings->digits);
m_ui->radio8Digits->setChecked(true); int index = m_ui->algorithmComboBox->findData(settings->algorithm);
} else if (settings->digits == 7) { if (index != -1) {
m_ui->radio7Digits->setChecked(true); m_ui->algorithmComboBox->setCurrentIndex(index);
} else {
m_ui->radio6Digits->setChecked(true);
} }
} }
} }

View File

@ -25,14 +25,26 @@
<item> <item>
<widget class="QLabel" name="label_3"> <widget class="QLabel" name="label_3">
<property name="text"> <property name="text">
<string>Key:</string> <string>Secret Key:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QLineEdit" name="seedEdit"> <widget class="QLineEdit" name="seedEdit">
<property name="sizePolicy">
<sizepolicy hsizetype="MinimumExpanding" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="minimumSize">
<size>
<width>150</width>
<height>0</height>
</size>
</property>
<property name="toolTip"> <property name="toolTip">
<string>Secret key in Base32 format</string> <string>Secret key must be in Base32 format</string>
</property> </property>
<property name="accessibleName"> <property name="accessibleName">
<string>Secret key field</string> <string>Secret key field</string>
@ -93,13 +105,10 @@
</widget> </widget>
</item> </item>
<item> <item>
<widget class="QGroupBox" name="customGroup"> <widget class="QGroupBox" name="customSettingsGroup">
<property name="enabled"> <property name="enabled">
<bool>false</bool> <bool>false</bool>
</property> </property>
<property name="styleSheet">
<string notr="true"/>
</property>
<property name="title"> <property name="title">
<string>Custom Settings</string> <string>Custom Settings</string>
</property> </property>
@ -113,20 +122,36 @@
<property name="labelAlignment"> <property name="labelAlignment">
<set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set> <set>Qt::AlignRight|Qt::AlignTop|Qt::AlignTrailing</set>
</property> </property>
<property name="horizontalSpacing">
<number>7</number>
</property>
<property name="verticalSpacing">
<number>7</number>
</property>
<property name="leftMargin"> <property name="leftMargin">
<number>5</number> <number>20</number>
</property> </property>
<property name="rightMargin"> <property name="rightMargin">
<number>5</number> <number>20</number>
</property> </property>
<item row="1" column="0"> <item row="1" column="0">
<widget class="QLabel" name="algorithmLabel">
<property name="text">
<string>Algorithm:</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QComboBox" name="algorithmComboBox"/>
</item>
<item row="2" column="0">
<widget class="QLabel" name="stepLabel"> <widget class="QLabel" name="stepLabel">
<property name="text"> <property name="text">
<string>Time step:</string> <string>Time step:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="1" column="1"> <item row="2" column="1">
<widget class="QSpinBox" name="stepSpinBox"> <widget class="QSpinBox" name="stepSpinBox">
<property name="accessibleName"> <property name="accessibleName">
<string>Time step field</string> <string>Time step field</string>
@ -145,34 +170,26 @@
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="0"> <item row="3" column="0">
<widget class="QLabel" name="digitsLabel"> <widget class="QLabel" name="digitsLabel">
<property name="text"> <property name="text">
<string>Code size:</string> <string>Code size:</string>
</property> </property>
</widget> </widget>
</item> </item>
<item row="2" column="1">
<widget class="QRadioButton" name="radio6Digits">
<property name="text">
<string>6 digits</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item row="3" column="1"> <item row="3" column="1">
<widget class="QRadioButton" name="radio7Digits"> <widget class="QSpinBox" name="digitsSpinBox">
<property name="text"> <property name="suffix">
<string>7 digits</string> <string> digits</string>
</property> </property>
</widget> <property name="minimum">
</item> <number>6</number>
<item row="4" column="1"> </property>
<widget class="QRadioButton" name="radio8Digits"> <property name="maximum">
<property name="text"> <number>10</number>
<string>8 digits</string> </property>
<property name="singleStep">
<number>1</number>
</property> </property>
</widget> </widget>
</item> </item>
@ -190,6 +207,9 @@
</widget> </widget>
</item> </item>
</layout> </layout>
<zorder>customSettingsGroup</zorder>
<zorder>buttonBox</zorder>
<zorder>groupBox</zorder>
</widget> </widget>
<tabstops> <tabstops>
<tabstop>seedEdit</tabstop> <tabstop>seedEdit</tabstop>
@ -197,9 +217,6 @@
<tabstop>radioSteam</tabstop> <tabstop>radioSteam</tabstop>
<tabstop>radioCustom</tabstop> <tabstop>radioCustom</tabstop>
<tabstop>stepSpinBox</tabstop> <tabstop>stepSpinBox</tabstop>
<tabstop>radio6Digits</tabstop>
<tabstop>radio7Digits</tabstop>
<tabstop>radio8Digits</tabstop>
</tabstops> </tabstops>
<resources/> <resources/>
<connections/> <connections/>

View File

@ -962,8 +962,9 @@ void EditEntryWidget::setForms(Entry* entry, bool restore)
#ifdef WITH_XC_BROWSER #ifdef WITH_XC_BROWSER
if (m_customData->contains(BrowserService::OPTION_SKIP_AUTO_SUBMIT)) { if (m_customData->contains(BrowserService::OPTION_SKIP_AUTO_SUBMIT)) {
m_browserUi->skipAutoSubmitCheckbox->setChecked(m_customData->value(BrowserService::OPTION_SKIP_AUTO_SUBMIT) // clang-format off
== "true"); m_browserUi->skipAutoSubmitCheckbox->setChecked(m_customData->value(BrowserService::OPTION_SKIP_AUTO_SUBMIT) == "true");
// clang-format on
} else { } else {
m_browserUi->skipAutoSubmitCheckbox->setChecked(false); m_browserUi->skipAutoSubmitCheckbox->setChecked(false);
} }

View File

@ -23,35 +23,34 @@
#include <QCryptographicHash> #include <QCryptographicHash>
#include <QMessageAuthenticationCode> #include <QMessageAuthenticationCode>
#include <QRegularExpression> #include <QRegularExpression>
#include <QRegularExpressionMatch>
#include <QUrl> #include <QUrl>
#include <QUrlQuery> #include <QUrlQuery>
#include <QVariant> #include <QVariant>
#include <QtEndian> #include <QtEndian>
#include <cmath> #include <cmath>
static QList<Totp::Encoder> encoders{ static QList<Totp::Encoder> totpEncoders{
{"", "", "0123456789", Totp::DEFAULT_DIGITS, Totp::DEFAULT_STEP, false}, {"", "", "0123456789", Totp::DEFAULT_DIGITS, Totp::DEFAULT_STEP, false},
{"steam", Totp::STEAM_SHORTNAME, "23456789BCDFGHJKMNPQRTVWXY", Totp::STEAM_DIGITS, Totp::DEFAULT_STEP, true}, {"steam", Totp::STEAM_SHORTNAME, "23456789BCDFGHJKMNPQRTVWXY", Totp::STEAM_DIGITS, Totp::DEFAULT_STEP, true},
}; };
static Totp::HashType getHashTypeByName(const QString& name) static Totp::Algorithm getHashTypeByName(const QString& name)
{ {
if (name.compare(QString("SHA512"), Qt::CaseInsensitive) == 0) { if (name.compare(QString("SHA512"), Qt::CaseInsensitive) == 0) {
return Totp::HashType::Sha512; return Totp::Algorithm::Sha512;
} }
if (name.compare(QString("SHA256"), Qt::CaseInsensitive) == 0) { if (name.compare(QString("SHA256"), Qt::CaseInsensitive) == 0) {
return Totp::HashType::Sha256; return Totp::Algorithm::Sha256;
} }
return Totp::HashType::Sha1; return Totp::Algorithm::Sha1;
} }
static QString getNameForHashType(const Totp::HashType hashType) static QString getNameForHashType(const Totp::Algorithm hashType)
{ {
switch (hashType) { switch (hashType) {
case Totp::HashType::Sha512: case Totp::Algorithm::Sha512:
return QString("SHA512"); return QString("SHA512");
case Totp::HashType::Sha256: case Totp::Algorithm::Sha256:
return QString("SHA256"); return QString("SHA256");
default: default:
return QString("SHA1"); return QString("SHA1");
@ -67,24 +66,26 @@ QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, c
if (url.isValid() && url.scheme() == "otpauth") { if (url.isValid() && url.scheme() == "otpauth") {
// Default OTP url format // Default OTP url format
QUrlQuery query(url); QUrlQuery query(url);
settings->otpUrl = true; settings->format = StorageFormat::OTPURL;
settings->key = query.queryItemValue("secret"); settings->key = query.queryItemValue("secret");
if (query.hasQueryItem("digits")) {
settings->digits = query.queryItemValue("digits").toUInt(); settings->digits = query.queryItemValue("digits").toUInt();
}
if (query.hasQueryItem("period")) {
settings->step = query.queryItemValue("period").toUInt(); settings->step = query.queryItemValue("period").toUInt();
}
if (query.hasQueryItem("encoder")) { if (query.hasQueryItem("encoder")) {
settings->encoder = getEncoderByName(query.queryItemValue("encoder")); settings->encoder = getEncoderByName(query.queryItemValue("encoder"));
} }
if (query.hasQueryItem("algorithm")) { if (query.hasQueryItem("algorithm")) {
settings->hashType = getHashTypeByName(query.queryItemValue("algorithm")); settings->algorithm = getHashTypeByName(query.queryItemValue("algorithm"));
} }
} else { } else {
QUrlQuery query(rawSettings); QUrlQuery query(rawSettings);
if (query.hasQueryItem("key")) { if (query.hasQueryItem("key")) {
// Compatibility with "KeeOtp" plugin // Compatibility with "KeeOtp" plugin
settings->keeOtp = true; settings->format = StorageFormat::KEEOTP;
settings->key = query.queryItemValue("key"); settings->key = query.queryItemValue("key");
settings->digits = DEFAULT_DIGITS;
settings->step = DEFAULT_STEP;
if (query.hasQueryItem("size")) { if (query.hasQueryItem("size")) {
settings->digits = query.queryItemValue("size").toUInt(); settings->digits = query.queryItemValue("size").toUInt();
} }
@ -92,10 +93,11 @@ QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, c
settings->step = query.queryItemValue("step").toUInt(); settings->step = query.queryItemValue("step").toUInt();
} }
if (query.hasQueryItem("otpHashMode")) { if (query.hasQueryItem("otpHashMode")) {
settings->hashType = getHashTypeByName(query.queryItemValue("otpHashMode")); settings->algorithm = getHashTypeByName(query.queryItemValue("otpHashMode"));
} }
} else { } else {
// Parse semi-colon separated values ([step];[digits|S]) // Parse semi-colon separated values ([step];[digits|S])
settings->format = StorageFormat::LEGACY;
auto vars = rawSettings.split(";"); auto vars = rawSettings.split(";");
if (vars.size() >= 2) { if (vars.size() >= 2) {
if (vars[1] == STEAM_SHORTNAME) { if (vars[1] == STEAM_SHORTNAME) {
@ -116,7 +118,8 @@ QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, c
// Detect custom settings, used by setup GUI // Detect custom settings, used by setup GUI
if (settings->encoder.shortName.isEmpty() if (settings->encoder.shortName.isEmpty()
&& (settings->digits != DEFAULT_DIGITS || settings->step != DEFAULT_STEP)) { && (settings->digits != DEFAULT_DIGITS || settings->step != DEFAULT_STEP
|| settings->algorithm != DEFAULT_ALGORITHM)) {
settings->custom = true; settings->custom = true;
} }
@ -126,23 +129,13 @@ QSharedPointer<Totp::Settings> Totp::parseSettings(const QString& rawSettings, c
QSharedPointer<Totp::Settings> Totp::createSettings(const QString& key, QSharedPointer<Totp::Settings> Totp::createSettings(const QString& key,
const uint digits, const uint digits,
const uint step, const uint step,
const Totp::StorageFormat format,
const QString& encoderShortName, const QString& encoderShortName,
const Totp::HashType hashType, const Totp::Algorithm algorithm)
QSharedPointer<Totp::Settings> prevSettings)
{ {
bool isCustom = digits != DEFAULT_DIGITS || step != DEFAULT_STEP; bool isCustom = digits != DEFAULT_DIGITS || step != DEFAULT_STEP || algorithm != DEFAULT_ALGORITHM;
if (prevSettings) { return QSharedPointer<Totp::Settings>(
prevSettings->key = key; new Totp::Settings{format, getEncoderByShortName(encoderShortName), algorithm, key, isCustom, digits, step});
prevSettings->hashType = hashType;
prevSettings->digits = digits;
prevSettings->step = step;
prevSettings->encoder = Totp::getEncoderByShortName(encoderShortName);
prevSettings->custom = isCustom;
return prevSettings;
} else {
return QSharedPointer<Totp::Settings>(new Totp::Settings{
getEncoderByShortName(encoderShortName), hashType, key, false, false, isCustom, digits, step});
}
} }
QString Totp::writeSettings(const QSharedPointer<Totp::Settings>& settings, QString Totp::writeSettings(const QSharedPointer<Totp::Settings>& settings,
@ -155,7 +148,7 @@ QString Totp::writeSettings(const QSharedPointer<Totp::Settings>& settings,
} }
// OTP Url output // OTP Url output
if (settings->otpUrl || forceOtp) { if (settings->format == StorageFormat::OTPURL || forceOtp) {
auto urlstring = QString("otpauth://totp/%1:%2?secret=%3&period=%4&digits=%5&issuer=%1") auto urlstring = QString("otpauth://totp/%1:%2?secret=%3&period=%4&digits=%5&issuer=%1")
.arg(title.isEmpty() ? "KeePassXC" : QString(QUrl::toPercentEncoding(title)), .arg(title.isEmpty() ? "KeePassXC" : QString(QUrl::toPercentEncoding(title)),
username.isEmpty() ? "none" : QString(QUrl::toPercentEncoding(username)), username.isEmpty() ? "none" : QString(QUrl::toPercentEncoding(username)),
@ -166,18 +159,18 @@ QString Totp::writeSettings(const QSharedPointer<Totp::Settings>& settings,
if (!settings->encoder.name.isEmpty()) { if (!settings->encoder.name.isEmpty()) {
urlstring.append("&encoder=").append(settings->encoder.name); urlstring.append("&encoder=").append(settings->encoder.name);
} }
if (settings->hashType != Totp::DEFAULT_HASHTYPE) { if (settings->algorithm != Totp::DEFAULT_ALGORITHM) {
urlstring.append("&algorithm=").append(getNameForHashType(settings->hashType)); urlstring.append("&algorithm=").append(getNameForHashType(settings->algorithm));
} }
return urlstring; return urlstring;
} else if (settings->keeOtp) { } else if (settings->format == StorageFormat::KEEOTP) {
// KeeOtp output // KeeOtp output
auto keyString = QString("key=%1&size=%2&step=%3") auto keyString = QString("key=%1&size=%2&step=%3")
.arg(QString(Base32::sanitizeInput(settings->key.toLatin1()))) .arg(QString(Base32::sanitizeInput(settings->key.toLatin1())))
.arg(settings->digits) .arg(settings->digits)
.arg(settings->step); .arg(settings->step);
if (settings->hashType != Totp::DEFAULT_HASHTYPE) { if (settings->algorithm != Totp::DEFAULT_ALGORITHM) {
keyString.append("&otpHashMode=").append(getNameForHashType(settings->hashType)); keyString.append("&otpHashMode=").append(getNameForHashType(settings->algorithm));
} }
return keyString; return keyString;
} else if (!settings->encoder.shortName.isEmpty()) { } else if (!settings->encoder.shortName.isEmpty()) {
@ -213,11 +206,11 @@ QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, const
} }
QCryptographicHash::Algorithm cryptoHash; QCryptographicHash::Algorithm cryptoHash;
switch (settings->hashType) { switch (settings->algorithm) {
case Totp::HashType::Sha512: case Totp::Algorithm::Sha512:
cryptoHash = QCryptographicHash::Sha512; cryptoHash = QCryptographicHash::Sha512;
break; break;
case Totp::HashType::Sha256: case Totp::Algorithm::Sha256:
cryptoHash = QCryptographicHash::Sha256; cryptoHash = QCryptographicHash::Sha256;
break; break;
default: default:
@ -256,11 +249,29 @@ QString Totp::generateTotp(const QSharedPointer<Totp::Settings>& settings, const
return retval; return retval;
} }
QList<QPair<QString, QString>> Totp::supportedEncoders()
{
QList<QPair<QString, QString>> encoders;
for (auto& encoder : totpEncoders) {
encoders << QPair<QString, QString>(encoder.name, encoder.shortName);
}
return encoders;
}
QList<QPair<QString, Totp::Algorithm>> Totp::supportedAlgorithms()
{
QList<QPair<QString, Algorithm>> algorithms;
algorithms << QPair<QString, Algorithm>(QStringLiteral("SHA-1"), Algorithm::Sha1);
algorithms << QPair<QString, Algorithm>(QStringLiteral("SHA-256"), Algorithm::Sha256);
algorithms << QPair<QString, Algorithm>(QStringLiteral("SHA-512"), Algorithm::Sha512);
return algorithms;
}
Totp::Encoder& Totp::defaultEncoder() Totp::Encoder& Totp::defaultEncoder()
{ {
// The first encoder is always the default // The first encoder is always the default
Q_ASSERT(!encoders.empty()); Q_ASSERT(!totpEncoders.empty());
return encoders[0]; return totpEncoders[0];
} }
Totp::Encoder& Totp::steamEncoder() Totp::Encoder& Totp::steamEncoder()
@ -270,7 +281,7 @@ Totp::Encoder& Totp::steamEncoder()
Totp::Encoder& Totp::getEncoderByShortName(const QString& shortName) Totp::Encoder& Totp::getEncoderByShortName(const QString& shortName)
{ {
for (auto& encoder : encoders) { for (auto& encoder : totpEncoders) {
if (encoder.shortName == shortName) { if (encoder.shortName == shortName) {
return encoder; return encoder;
} }
@ -280,7 +291,7 @@ Totp::Encoder& Totp::getEncoderByShortName(const QString& shortName)
Totp::Encoder& Totp::getEncoderByName(const QString& name) Totp::Encoder& Totp::getEncoderByName(const QString& name)
{ {
for (auto& encoder : encoders) { for (auto& encoder : totpEncoders) {
if (encoder.name == name) { if (encoder.name == name) {
return encoder; return encoder;
} }

View File

@ -28,7 +28,6 @@ class QUrl;
namespace Totp namespace Totp
{ {
struct Encoder struct Encoder
{ {
QString name; QString name;
@ -39,20 +38,26 @@ namespace Totp
bool reverse; bool reverse;
}; };
enum HashType enum Algorithm
{ {
Sha1, Sha1,
Sha256, Sha256,
Sha512, Sha512,
}; };
enum StorageFormat
{
OTPURL,
KEEOTP,
LEGACY,
};
struct Settings struct Settings
{ {
Totp::StorageFormat format;
Totp::Encoder encoder; Totp::Encoder encoder;
Totp::HashType hashType; Totp::Algorithm algorithm;
QString key; QString key;
bool otpUrl;
bool keeOtp;
bool custom; bool custom;
uint digits; uint digits;
uint step; uint step;
@ -61,7 +66,8 @@ namespace Totp
constexpr uint DEFAULT_STEP = 30u; constexpr uint DEFAULT_STEP = 30u;
constexpr uint DEFAULT_DIGITS = 6u; constexpr uint DEFAULT_DIGITS = 6u;
constexpr uint STEAM_DIGITS = 5u; constexpr uint STEAM_DIGITS = 5u;
constexpr Totp::HashType DEFAULT_HASHTYPE = Sha1; constexpr Totp::Algorithm DEFAULT_ALGORITHM = Sha1;
constexpr Totp::StorageFormat DEFAULT_FORMAT = OTPURL;
static const QString STEAM_SHORTNAME = "S"; static const QString STEAM_SHORTNAME = "S";
static const QString ATTRIBUTE_OTP = "otp"; static const QString ATTRIBUTE_OTP = "otp";
@ -72,9 +78,9 @@ namespace Totp
QSharedPointer<Totp::Settings> createSettings(const QString& key, QSharedPointer<Totp::Settings> createSettings(const QString& key,
const uint digits, const uint digits,
const uint step, const uint step,
const Totp::StorageFormat format = DEFAULT_FORMAT,
const QString& encoderShortName = {}, const QString& encoderShortName = {},
const Totp::HashType hashType = DEFAULT_HASHTYPE, const Totp::Algorithm algorithm = DEFAULT_ALGORITHM);
QSharedPointer<Totp::Settings> prevSettings = {});
QString writeSettings(const QSharedPointer<Totp::Settings>& settings, QString writeSettings(const QSharedPointer<Totp::Settings>& settings,
const QString& title = {}, const QString& title = {},
const QString& username = {}, const QString& username = {},
@ -82,6 +88,9 @@ namespace Totp
QString generateTotp(const QSharedPointer<Totp::Settings>& settings, const quint64 time = 0ull); QString generateTotp(const QSharedPointer<Totp::Settings>& settings, const quint64 time = 0ull);
QList<QPair<QString, QString>> supportedEncoders();
QList<QPair<QString, Algorithm>> supportedAlgorithms();
Encoder& defaultEncoder(); Encoder& defaultEncoder();
Encoder& steamEncoder(); Encoder& steamEncoder();
Encoder& getEncoderByShortName(const QString& shortName); Encoder& getEncoderByShortName(const QString& shortName);

View File

@ -39,9 +39,10 @@ void TestTotp::testParseSecret()
QVERIFY(!settings.isNull()); QVERIFY(!settings.isNull());
QCOMPARE(settings->key, QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ")); QCOMPARE(settings->key, QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"));
QCOMPARE(settings->custom, false); QCOMPARE(settings->custom, false);
QCOMPARE(settings->format, Totp::StorageFormat::OTPURL);
QCOMPARE(settings->digits, 6u); QCOMPARE(settings->digits, 6u);
QCOMPARE(settings->step, 30u); QCOMPARE(settings->step, 30u);
QCOMPARE(settings->hashType, Totp::HashType::Sha1); QCOMPARE(settings->algorithm, Totp::Algorithm::Sha1);
// OTP URL with non-default hash type // OTP URL with non-default hash type
secret = "otpauth://totp/" secret = "otpauth://totp/"
@ -50,10 +51,11 @@ void TestTotp::testParseSecret()
settings = Totp::parseSettings(secret); settings = Totp::parseSettings(secret);
QVERIFY(!settings.isNull()); QVERIFY(!settings.isNull());
QCOMPARE(settings->key, QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ")); QCOMPARE(settings->key, QString("HXDMVJECJJWSRB3HWIZR4IFUGFTMXBOZ"));
QCOMPARE(settings->custom, false); QCOMPARE(settings->custom, true);
QCOMPARE(settings->format, Totp::StorageFormat::OTPURL);
QCOMPARE(settings->digits, 6u); QCOMPARE(settings->digits, 6u);
QCOMPARE(settings->step, 30u); QCOMPARE(settings->step, 30u);
QCOMPARE(settings->hashType, Totp::HashType::Sha512); QCOMPARE(settings->algorithm, Totp::Algorithm::Sha512);
// KeeOTP Parsing // KeeOTP Parsing
secret = "key=HXDMVJECJJWSRBY%3d&step=25&size=8&otpHashMode=Sha256"; secret = "key=HXDMVJECJJWSRBY%3d&step=25&size=8&otpHashMode=Sha256";
@ -61,9 +63,10 @@ void TestTotp::testParseSecret()
QVERIFY(!settings.isNull()); QVERIFY(!settings.isNull());
QCOMPARE(settings->key, QString("HXDMVJECJJWSRBY=")); QCOMPARE(settings->key, QString("HXDMVJECJJWSRBY="));
QCOMPARE(settings->custom, true); QCOMPARE(settings->custom, true);
QCOMPARE(settings->format, Totp::StorageFormat::KEEOTP);
QCOMPARE(settings->digits, 8u); QCOMPARE(settings->digits, 8u);
QCOMPARE(settings->step, 25u); QCOMPARE(settings->step, 25u);
QCOMPARE(settings->hashType, Totp::HashType::Sha256); QCOMPARE(settings->algorithm, Totp::Algorithm::Sha256);
// Semi-colon delineated "TOTP Settings" // Semi-colon delineated "TOTP Settings"
secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq"; secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
@ -71,9 +74,10 @@ void TestTotp::testParseSecret()
QVERIFY(!settings.isNull()); QVERIFY(!settings.isNull());
QCOMPARE(settings->key, QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq")); QCOMPARE(settings->key, QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq"));
QCOMPARE(settings->custom, true); QCOMPARE(settings->custom, true);
QCOMPARE(settings->format, Totp::StorageFormat::LEGACY);
QCOMPARE(settings->digits, 8u); QCOMPARE(settings->digits, 8u);
QCOMPARE(settings->step, 30u); QCOMPARE(settings->step, 30u);
QCOMPARE(settings->hashType, Totp::HashType::Sha1); QCOMPARE(settings->algorithm, Totp::Algorithm::Sha1);
// Bare secret (no "TOTP Settings" attribute) // Bare secret (no "TOTP Settings" attribute)
secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq"; secret = "gezdgnbvgy3tqojqgezdgnbvgy3tqojq";
@ -81,9 +85,10 @@ void TestTotp::testParseSecret()
QVERIFY(!settings.isNull()); QVERIFY(!settings.isNull());
QCOMPARE(settings->key, QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq")); QCOMPARE(settings->key, QString("gezdgnbvgy3tqojqgezdgnbvgy3tqojq"));
QCOMPARE(settings->custom, false); QCOMPARE(settings->custom, false);
QCOMPARE(settings->format, Totp::StorageFormat::LEGACY);
QCOMPARE(settings->digits, 6u); QCOMPARE(settings->digits, 6u);
QCOMPARE(settings->step, 30u); QCOMPARE(settings->step, 30u);
QCOMPARE(settings->hashType, Totp::HashType::Sha1); QCOMPARE(settings->algorithm, Totp::Algorithm::Sha1);
} }
void TestTotp::testTotpCode() void TestTotp::testTotpCode()
@ -119,6 +124,7 @@ void TestTotp::testSteamTotp()
QCOMPARE(settings->key, QString("63BEDWCQZKTQWPESARIERL5DTTQFCJTK")); QCOMPARE(settings->key, QString("63BEDWCQZKTQWPESARIERL5DTTQFCJTK"));
QCOMPARE(settings->encoder.shortName, Totp::STEAM_SHORTNAME); QCOMPARE(settings->encoder.shortName, Totp::STEAM_SHORTNAME);
QCOMPARE(settings->format, Totp::StorageFormat::OTPURL);
QCOMPARE(settings->digits, Totp::STEAM_DIGITS); QCOMPARE(settings->digits, Totp::STEAM_DIGITS);
QCOMPARE(settings->step, 30u); QCOMPARE(settings->step, 30u);