Add initial Steam TOTP support

* Add the concept of custom TOTP encoders, each with potential for custom
  code alphabet, length, step interval and code direction (i.e. reversed)
* Select custom encoder via overload of the digits field of a loaded entry
* Allow selection of custom encoders via the "TOTP Settings" field's
  size, as currently done by KeeTrayTOTP for Steam. Use "S" for the
  short name of the Steam custom encoder
* Allow selection of custom encoders via the "otp" field by appending
  a "&encoder=<name>" field to the URL query. For example,
  "&encoder=steam"
* Update TOTP set-up dialog to permit selection between (default,
  steam, custom) settings.
This commit is contained in:
Joel Smith
2017-11-20 11:01:22 -07:00
parent 1cb91fabb6
commit 8ca52ba8f9
11 changed files with 270 additions and 34 deletions

View File

@@ -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()
{
}
@@ -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);
@@ -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
@@ -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)
{
QUrl keyUri;
keyUri.setScheme("otpauth");

View File

@@ -20,6 +20,8 @@
#define QTOTP_H
#include <QtCore/qglobal.h>
#include <QString>
#include <QMap>
class QUrl;
@@ -34,10 +36,25 @@ public:
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