Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add initial Steam TOTP support #1206

Merged
merged 1 commit into from
Nov 21, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 26 additions & 7 deletions src/core/Entry.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,8 @@
#include "core/Metadata.h"
#include "totp/totp.h"

#include <QRegularExpression>

const int Entry::DefaultIconNumber = 0;
const int Entry::ResolveMaximumDepth = 10;

Expand Down Expand Up @@ -332,12 +334,24 @@ void Entry::setTotp(const QString& seed, quint8& step, quint8& digits)
if (digits == 0) {
digits = QTotp::defaultDigits;
}
QString data;

const QTotp::Encoder & enc = QTotp::encoders.value(digits, QTotp::defaultEncoder);

if (m_attributes->hasKey("otp")) {
m_attributes->set("otp", QString("key=%1&step=%2&size=%3").arg(seed).arg(step).arg(digits), true);
data = QString("key=%1&step=%2&size=%3").arg(seed).arg(step).arg(enc.digits == 0 ? digits : enc.digits);
if (!enc.name.isEmpty()) {
data.append("&enocder=").append(enc.name);
}
m_attributes->set("otp", data, true);
} else {
m_attributes->set("TOTP Seed", seed, true);
m_attributes->set("TOTP Settings", QString("%1;%2").arg(step).arg(digits));
if (!enc.shortName.isEmpty()) {
data = QString("%1;%2").arg(step).arg(enc.shortName);
} else {
data = QString("%1;%2").arg(step).arg(digits);
}
m_attributes->set("TOTP Settings", data);
}
}

Expand All @@ -355,11 +369,16 @@ QString Entry::totpSeed() const
m_data.totpStep = QTotp::defaultStep;

if (m_attributes->hasKey("TOTP Settings")) {
QRegExp rx("(\\d+);(\\d)", Qt::CaseInsensitive, QRegExp::RegExp);
int pos = rx.indexIn(m_attributes->value("TOTP Settings"));
if (pos > -1) {
m_data.totpStep = rx.cap(1).toUInt();
m_data.totpDigits = rx.cap(2).toUInt();
// this regex must be kept in sync with the set of allowed short names QTotp::shortNameToEncoder
QRegularExpression rx(QString("(\\d+);((?:\\d+)|S)"));
QRegularExpressionMatch m = rx.match(m_attributes->value("TOTP Settings"));
if (m.hasMatch()) {
m_data.totpStep = m.captured(1).toUInt();
if (QTotp::shortNameToEncoder.contains(m.captured(2))) {
m_data.totpDigits = QTotp::shortNameToEncoder[m.captured(2)];
} else {
m_data.totpDigits = m.captured(2).toUInt();
}
}
}

Expand Down
2 changes: 2 additions & 0 deletions src/gui/DatabaseWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -417,6 +417,8 @@ void DatabaseWidget::setupTotp()
setupTotpDialog->setSeed(currentEntry->totpSeed());
setupTotpDialog->setStep(currentEntry->totpStep());
setupTotpDialog->setDigits(currentEntry->totpDigits());
// now that all settings are set, decide whether it's default, steam or custom
setupTotpDialog->setSettings(currentEntry->totpDigits());
}

setupTotpDialog->open();
Expand Down
2 changes: 1 addition & 1 deletion src/gui/DetailsWidget.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,7 @@ void DetailsWidget::updateTotp()
if (!m_locked) {
QString totpCode = m_currentEntry->totp();
QString firstHalf = totpCode.left(totpCode.size() / 2);
QString secondHalf = totpCode.right(totpCode.size() / 2);
QString secondHalf = totpCode.mid(totpCode.size() / 2);
m_ui->totpLabel->setText(firstHalf + " " + secondHalf);
} else if (nullptr != m_timer) {
m_timer->stop();
Expand Down
49 changes: 38 additions & 11 deletions src/gui/SetupTotpDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,15 +35,19 @@ SetupTotpDialog::SetupTotpDialog(DatabaseWidget* parent, Entry* entry)

connect(m_ui->buttonBox, SIGNAL(rejected()), SLOT(close()));
connect(m_ui->buttonBox, SIGNAL(accepted()), SLOT(setupTotp()));
connect(m_ui->customSettingsCheckBox, SIGNAL(toggled(bool)), SLOT(toggleCustom(bool)));
connect(m_ui->radioDefault, SIGNAL(toggled(bool)), SLOT(toggleDefault(bool)));
connect(m_ui->radioSteam, SIGNAL(toggled(bool)), SLOT(toggleSteam(bool)));
connect(m_ui->radioCustom, SIGNAL(toggled(bool)), SLOT(toggleCustom(bool)));
}


void SetupTotpDialog::setupTotp()
{
quint8 digits;

if (m_ui->radio8Digits->isChecked()) {
if (m_ui->radioSteam->isChecked()) {
digits = QTotp::ENCODER_STEAM;
} else if (m_ui->radio8Digits->isChecked()) {
digits = 8;
} else {
digits = 6;
Expand All @@ -56,6 +60,22 @@ void SetupTotpDialog::setupTotp()
close();
}

void SetupTotpDialog::toggleDefault(bool status)
{
if (status) {
setStep(QTotp::defaultStep);
setDigits(QTotp::defaultDigits);
}
}

void SetupTotpDialog::toggleSteam(bool status)
{
if (status) {
setStep(QTotp::defaultStep);
setDigits(QTotp::ENCODER_STEAM);
}
}

void SetupTotpDialog::toggleCustom(bool status)
{
m_ui->digitsLabel->setEnabled(status);
Expand All @@ -72,13 +92,25 @@ void SetupTotpDialog::setSeed(QString value)
m_ui->seedEdit->setText(value);
}

void SetupTotpDialog::setSettings(quint8 digits) {
quint8 step = m_ui->stepSpinBox->value();

bool isDefault = ((step == QTotp::defaultStep) &&
(digits == QTotp::defaultDigits));
bool isSteam = (digits == QTotp::ENCODER_STEAM);

if (isSteam) {
m_ui->radioSteam->setChecked(true);
} else if (isDefault) {
m_ui->radioDefault->setChecked(true);
} else {
m_ui->radioCustom->setChecked(true);
}
}

void SetupTotpDialog::setStep(quint8 step)
{
m_ui->stepSpinBox->setValue(step);

if (step != QTotp::defaultStep) {
m_ui->customSettingsCheckBox->setChecked(true);
}
}

void SetupTotpDialog::setDigits(quint8 digits)
Expand All @@ -90,13 +122,8 @@ void SetupTotpDialog::setDigits(quint8 digits)
m_ui->radio6Digits->setChecked(true);
m_ui->radio8Digits->setChecked(false);
}

if (digits != QTotp::defaultDigits) {
m_ui->customSettingsCheckBox->setChecked(true);
}
}


SetupTotpDialog::~SetupTotpDialog()
{
}
3 changes: 3 additions & 0 deletions src/gui/SetupTotpDialog.h
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,11 @@ class SetupTotpDialog : public QDialog
void setSeed(QString value);
void setStep(quint8 step);
void setDigits(quint8 digits);
void setSettings(quint8 digits);

private Q_SLOTS:
void toggleDefault(bool status);
void toggleSteam(bool status);
void toggleCustom(bool status);
void setupTotp();

Expand Down
42 changes: 36 additions & 6 deletions src/gui/SetupTotpDialog.ui
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>282</width>
<height>257</height>
<height>364</height>
</rect>
</property>
<property name="windowTitle">
Expand All @@ -29,11 +29,38 @@
</layout>
</item>
<item>
<widget class="QCheckBox" name="customSettingsCheckBox">
<property name="text">
<string>Use custom settings</string>
</property>
</widget>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QRadioButton" name="radioDefault">
<property name="text">
<string>Default RFC 6238 token settings</string>
</property>
<attribute name="buttonGroup">
<string notr="true">settingsButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioSteam">
<property name="text">
<string>Steam token settings</string>
</property>
<attribute name="buttonGroup">
<string notr="true">settingsButtonGroup</string>
</attribute>
</widget>
</item>
<item>
<widget class="QRadioButton" name="radioCustom">
<property name="text">
<string>Use custom settings</string>
</property>
<attribute name="buttonGroup">
<string notr="true">settingsButtonGroup</string>
</attribute>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QLabel" name="label_4">
Expand Down Expand Up @@ -134,4 +161,7 @@
</widget>
<resources/>
<connections/>
<buttongroups>
<buttongroup name="settingsButtonGroup"/>
</buttongroups>
</ui>
4 changes: 2 additions & 2 deletions src/gui/TotpDialog.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -87,8 +87,8 @@ void TotpDialog::updateSeconds()
void TotpDialog::updateTotp()
{
QString totpCode = m_entry->totp();
QString firstHalf = totpCode.left(totpCode.size()/2);
QString secondHalf = totpCode.right(totpCode.size()/2);
QString firstHalf = totpCode.left(totpCode.size() / 2);
QString secondHalf = totpCode.mid(totpCode.size() / 2);
m_ui->totpLabel->setText(firstHalf + " " + secondHalf);
}

Expand Down
60 changes: 55 additions & 5 deletions src/totp/totp.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,39 @@
const quint8 QTotp::defaultStep = 30;
const quint8 QTotp::defaultDigits = 6;

/**
* Custom encoder types. Each should be unique and >= 128 and < 255
* Values have no meaning outside of keepassxc
*/
/**
* Encoder for Steam Guard TOTP
*/
const quint8 QTotp::ENCODER_STEAM = 254;

const QTotp::Encoder QTotp::defaultEncoder = { "", "", "0123456789", 0, 0, false };
const QMap<quint8, QTotp::Encoder> QTotp::encoders{
{ QTotp::ENCODER_STEAM, { "steam", "S", "23456789BCDFGHJKMNPQRTVWXY", 5, 30, true } },
};

/**
* These map the second field of the "TOTP Settings" field to our internal encoder number
* that overloads the digits field. Make sure that the key matches the shortName value
* in the corresponding Encoder
* NOTE: when updating this map, a corresponding edit to the settings regex must be made
* in Entry::totpSeed()
*/
const QMap<QString, quint8> QTotp::shortNameToEncoder{
{ "S", QTotp::ENCODER_STEAM },
};
/**
* These map the "encoder=" URL parameter of the "otp" field to our internal encoder number
* that overloads the digits field. Make sure that the key matches the name value
* in the corresponding Encoder
*/
const QMap<QString, quint8> QTotp::nameToEncoder{
{ "steam", QTotp::ENCODER_STEAM },
};

QTotp::QTotp()
{
}
Expand All @@ -57,7 +90,10 @@ QString QTotp::parseOtpString(QString key, quint8& digits, quint8& step)
if (q_step > 0 && q_step <= 60) {
step = q_step;
}

QString encName = query.queryItemValue("encoder");
if (!encName.isEmpty() && nameToEncoder.contains(encName)) {
digits = nameToEncoder[encName];
}
} else {
// Compatibility with "KeeOtp" plugin string format
QRegExp rx("key=(.+)", Qt::CaseInsensitive, QRegExp::RegExp);
Expand Down Expand Up @@ -119,10 +155,24 @@ QString QTotp::generateTotp(const QByteArray key,
| (hmac[offset + 3] & 0xff);
// clang-format on

quint32 digitsPower = pow(10, numDigits);
const Encoder& encoder = encoders.value(numDigits, defaultEncoder);
// if encoder.digits is 0, we need to use the passed-in number of digits (default encoder)
quint8 digits = encoder.digits == 0 ? numDigits : encoder.digits;
int direction = -1;
int startpos = digits - 1;
if (encoder.reverse) {
direction = 1;
startpos = 0;
}
quint32 digitsPower = pow(encoder.alphabet.size(), digits);

quint64 password = binary % digitsPower;
return QString("%1").arg(password, numDigits, 10, QChar('0'));
QString retval(int(digits), encoder.alphabet[0]);
for (quint8 pos = startpos; password > 0; pos += direction) {
retval[pos] = encoder.alphabet[int(password % encoder.alphabet.size())];
password /= encoder.alphabet.size();
}
return retval;
}

// See: https://github.com/google/google-authenticator/wiki/Key-Uri-Format
Expand All @@ -131,8 +181,8 @@ QUrl QTotp::generateOtpString(const QString& secret,
const QString& issuer,
const QString& username,
const QString& algorithm,
const quint8& digits,
const quint8& step)
quint8 digits,
quint8 step)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

😁

{
QUrl keyUri;
keyUri.setScheme("otpauth");
Expand Down
21 changes: 19 additions & 2 deletions src/totp/totp.h
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
#define QTOTP_H

#include <QtCore/qglobal.h>
#include <QString>
#include <QMap>

class QUrl;

Expand All @@ -34,10 +36,25 @@ class QTotp
const QString& issuer,
const QString& username,
const QString& algorithm,
const quint8& digits,
const quint8& step);
quint8 digits,
quint8 step);
static const quint8 defaultStep;
static const quint8 defaultDigits;
struct Encoder
{
QString name;
QString shortName;
QString alphabet;
quint8 digits;
quint8 step;
bool reverse;
};
static const Encoder defaultEncoder;
// custom encoder values that overload the digits field
static const quint8 ENCODER_STEAM;
static const QMap<quint8, Encoder> encoders;
static const QMap<QString, quint8> shortNameToEncoder;
static const QMap<QString, quint8> nameToEncoder;
};

#endif // QTOTP_H
Loading