File indexing completed on 2024-04-28 05:50:09

0001 /*
0002  * SPDX-License-Identifier: GPL-3.0-or-later
0003  * SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
0004  */
0005 #include "oath.h"
0006 
0007 #include "../hmac/hmac.h"
0008 #include "../logging_p.h"
0009 
0010 #include <limits>
0011 
0012 KEYSMITH_LOGGER(logger, ".oath")
0013 
0014 static qint64 maxMSecsOffset = std::numeric_limits<qint64>::max();
0015 
0016 static QString encodeDefaults(quint32 value, uint tokenLength)
0017 {
0018     Q_ASSERT_X(tokenLength >= 6, Q_FUNC_INFO, "token length should be at least 6 characters long");
0019 
0020     QString base;
0021     base.setNum(value, 10);
0022 
0023     QString prefix(QLatin1String(""));
0024     for (uint i = base.size(); i < tokenLength; ++i) {
0025         prefix += QLatin1Char('0');
0026     }
0027 
0028     return prefix + base;
0029 }
0030 
0031 static QString encodeDefaultsWithChecksum(quint32 value, uint tokenLength)
0032 {
0033     QString prefix = encodeDefaults(value, tokenLength);
0034 
0035     QString check;
0036     check.setNum(oath::luhnChecksum(value, tokenLength), 10);
0037 
0038     return prefix + check;
0039 }
0040 
0041 static quint32 truncate(const QByteArray &hash, uint offset)
0042 {
0043     Q_ASSERT_X(hash.size() >= 4, Q_FUNC_INFO, "hash output is too small");
0044     Q_ASSERT_X(offset <= (((uint) hash.size()) - 4UL), Q_FUNC_INFO, "truncation offset is too large for the hash output");
0045 
0046     return ((((quint32) hash[offset]) & 0x7FUL) << 24)
0047         | ((((quint32) hash[offset + 1]) & 0xFFUL) << 16)
0048         | ((((quint32) hash[offset + 2]) & 0xFFUL) << 8)
0049         | (((quint32) hash[offset + 3]) & 0xFFUL);
0050 }
0051 
0052 static quint32 truncateDynamically(const QByteArray &hash)
0053 {
0054     Q_ASSERT_X(hash.size() >= 20, Q_FUNC_INFO, "hash output is too small");
0055     return truncate(hash, ((uint) hash[hash.size() - 1]) & 0x0FUL);
0056 }
0057 
0058 namespace oath
0059 {
0060     Encoder::Encoder(uint tokenLength, bool addChecksum) : m_tokenLength(tokenLength), m_addChecksum(addChecksum)
0061     {
0062     }
0063 
0064     Encoder::~Encoder()
0065     {
0066     }
0067 
0068     quint32 Encoder::reduceMod10(quint32 value, uint tokenLength)
0069     {
0070         /*
0071          * Skip modulo 10 reduction for tokens of 10 or more characters:
0072          * the value is already guaranteed to be in its modulo 10 reduced form, because 2^32 is less than 10^10.
0073          * This check also takes care of possible integer overflow, for the same reason.
0074          */
0075         return tokenLength <= 9 ? value % powerTable[tokenLength] : value;
0076     }
0077 
0078     QString Encoder::encode(quint32 value) const
0079     {
0080         value = reduceMod10(value, m_tokenLength);
0081         return m_addChecksum ? encodeDefaultsWithChecksum(value, m_tokenLength) : encodeDefaults(value, m_tokenLength);
0082     }
0083 
0084     uint Encoder::tokenLength(void) const
0085     {
0086         return m_tokenLength;
0087     }
0088 
0089     bool Encoder::checksum(void) const
0090     {
0091         return m_addChecksum;
0092     }
0093 
0094     bool Algorithm::validate(const Encoder *encoder)
0095     {
0096         // HOTP spec mandates a minimum token length of 6 digits
0097         return encoder && encoder->tokenLength() >= 6;
0098     }
0099 
0100     bool Algorithm::validate(QCryptographicHash::Algorithm algorithm, const std::optional<uint> offset)
0101     {
0102         /*
0103          * An nullopt offset indicates dynamic truncation.
0104          * Dynamic truncation works by taking the last nible and interpreting it as offset for truncation, i.e. it will always be <= 15.
0105          * Accounting for the last nibble (therefore last byte) assume a max truncation offset of 16 if dynamic truncation is used.
0106          */
0107         uint truncateAt = offset ? *offset : 16U;
0108 
0109         /*
0110          * The given algorithm must be supported/have a known digest size.
0111          * There must be at least 4 bytes available at the given truncation offset/limit.
0112          */
0113         std::optional<uint> digestSize = hmac::outputSize(algorithm);
0114         return digestSize && *digestSize >= 4U && (*digestSize - 4U) >= truncateAt;
0115     }
0116 
0117     std::optional<Algorithm> Algorithm::create(QCryptographicHash::Algorithm algorithm, const std::optional<uint> offset, const QSharedPointer<const Encoder> &encoder, bool requireSaneKeyLength)
0118     {
0119         if(!validate(algorithm, offset)) {
0120             qCDebug(logger) << "Invalid algorithm:" << algorithm << "or incompatible with truncation offset:" << (offset ? *offset : 16U);
0121             return std::nullopt;
0122         }
0123 
0124         if (!encoder || !validate(encoder.data())) {
0125             qCDebug(logger) << "Invalid token encoder";
0126             return std::nullopt;
0127         }
0128 
0129         std::function<quint32(const QByteArray &)> truncation(truncateDynamically);
0130         if (offset) {
0131             uint at = *offset;
0132             truncation = [at](const QByteArray &bytes) -> quint32
0133             {
0134                 return truncate(bytes, at);
0135             };
0136         }
0137 
0138         return std::optional<Algorithm>(Algorithm(encoder, truncation, algorithm, requireSaneKeyLength));
0139     }
0140 
0141     std::optional<Algorithm> Algorithm::totp(QCryptographicHash::Algorithm algorithm, uint tokenLength, bool requireSaneKeyLength)
0142     {
0143         const QSharedPointer<const Encoder> encoder(new Encoder(tokenLength, false));
0144         return create(algorithm, std::nullopt, encoder, requireSaneKeyLength);
0145     }
0146 
0147     std::optional<Algorithm> Algorithm::hotp(const std::optional<uint> offset, uint tokenLength, bool checksum, bool requireSaneKeyLength)
0148     {
0149         const QSharedPointer<const Encoder> encoder(new Encoder(tokenLength, checksum));
0150         return create(QCryptographicHash::Sha1, offset, encoder, requireSaneKeyLength);
0151     }
0152 
0153     Algorithm::Algorithm(const QSharedPointer<const Encoder> &encoder, const std::function<quint32(const QByteArray &)> &truncation, QCryptographicHash::Algorithm algorithm, bool requireSaneKeyLength) :
0154         m_encoder(encoder), m_truncation(truncation), m_enforceKeyLength(requireSaneKeyLength), m_algorithm(algorithm)
0155     {
0156     }
0157 
0158     std::optional<QString> Algorithm::compute(quint64 counter, char * secretBuffer, int length) const
0159     {
0160         if (!secretBuffer) {
0161             return std::nullopt;
0162         }
0163 
0164         if (!hmac::validateKeySize(m_algorithm, length, m_enforceKeyLength)) {
0165             qCDebug(logger)
0166                 << "Invalid key size:" << length << "for algorithm:" << m_algorithm
0167                 << "Sane key length requirements apply:" << m_enforceKeyLength;
0168             return std::nullopt;
0169         }
0170 
0171         QByteArray message;
0172         message.resize(8);
0173 
0174         for (int i = 0; i < 8; ++i) {
0175             message[i] = (char) ((counter >> (56 - i * 8)) & 0xFFULL);
0176         }
0177 
0178         std::optional<QByteArray> digest = hmac::compute(m_algorithm, secretBuffer, length, message, m_enforceKeyLength);
0179         if (digest) {
0180             quint32 result = m_truncation(*digest);
0181             result = Encoder::reduceMod10(result, m_encoder->tokenLength());
0182             return std::optional<QString>(m_encoder->encode(result));
0183         }
0184 
0185         qCDebug(logger) << "Failed to compute token";
0186         return std::nullopt;
0187     }
0188 
0189     uint luhnChecksum(quint32 value, uint digits)
0190     {
0191         static const uint lookupTable[10] = {
0192             0, // 0 * 2
0193             2, // 1 * 2
0194             4, // 2 * 2
0195             6, // 3 * 2
0196             8, // 4 * 2
0197             1, // 5 * 2 - 9
0198             3, // 6 * 2 - 9
0199             5, // 7 * 2 - 9
0200             7, // 8 * 2 - 9
0201             9, // 9 * 2 - 9
0202         };
0203 
0204         Q_ASSERT_X(digits > 0UL, Q_FUNC_INFO, "checksum cannot be computed over less than 1 digit");
0205         uint sum = 0UL;
0206         bool doubledMinus9 = true;
0207         for (uint d = 0UL; d < digits && value != 0UL; ++d) {
0208             uint position = value % 10UL;
0209 
0210             sum += doubledMinus9 ? lookupTable[position] : position;
0211 
0212             value /= 10UL;
0213             doubledMinus9 = !doubledMinus9;
0214         }
0215 
0216         sum = sum % 10ULL;
0217         return sum == 0UL ? 0UL : 10UL - sum;
0218     }
0219 
0220     std::optional<quint64> count(const QDateTime &epoch, uint timeStep, const std::function<qint64(void)> &clock)
0221     {
0222         qint64 epochMillis = epoch.toMSecsSinceEpoch();
0223         qint64 now = clock();
0224 
0225         if (now < epochMillis) {
0226             qCDebug(logger) << "Unable to count time steps: epoch is in the future";
0227             return std::nullopt;
0228         }
0229 
0230         if (timeStep == 0UL) {
0231             qCDebug(logger) << "Unable to count time steps: invalid step size:" << timeStep;
0232             return std::nullopt;
0233         }
0234 
0235         quint64 msecs = ((quint64) (now - epochMillis));
0236         quint64 stepInMsecs = ((quint64) timeStep) * 1000ULL;
0237         return std::optional<quint64>(msecs / stepInMsecs);
0238     }
0239 
0240     /*
0241      * Converts a negative qint64 value to its absolute value equivalent in quint64.
0242      */
0243     static quint64 flipSign(qint64 value)
0244     {
0245         static const quint64 max = std::numeric_limits<quint64>::max();
0246         // take advantage of two's complement to simplify this
0247         return max - ((quint64) value) + 1ULL;
0248     }
0249 
0250     std::optional<QDateTime> fromCounter(quint64 count, const QDateTime &epoch, uint timeStep)
0251     {
0252         qint64 epochMillis = epoch.toMSecsSinceEpoch();
0253 
0254         /*
0255          * Calculate the number of milliseconds that would be available for the given token.
0256          */
0257         quint64 max = epochMillis >= 0
0258             ? (quint64) (maxMSecsOffset - epochMillis)
0259             : ((quint64) maxMSecsOffset) + flipSign(epochMillis);
0260 
0261         quint64 step = timeStep * 1000ULL;
0262 
0263         // see if the requested count of time steps 'fits' inside the number of available milliseconds
0264         if ((max / step) < count) {
0265             qCDebug(logger)
0266                 << "Unable to compute datetime matching the given count of time steps:"
0267                 << "Storage type not wide enough, not enough milliseconds available";
0268             return std::nullopt;
0269         }
0270 
0271         quint64 ms = count * step;
0272         qint64 offset = epochMillis;
0273         if (ms <= ((quint64) maxMSecsOffset)) {
0274             offset += (qint64) ms;
0275         } else {
0276             /*
0277              * This is safe to do because:
0278              * - it has been verified that the number of requested steps 'fits' within the number of available ms
0279              * - therefore the epoch must have been negative
0280              */
0281             offset += maxMSecsOffset;
0282             ms -= (quint64) maxMSecsOffset;
0283             offset += (qint64) ms;
0284         }
0285 
0286         /*
0287          * QDateTime::fromMSecsSinceEpoch() is documented that it cannot handle the full qint64 width, but it is not
0288          * documented what exactly the restrictions are. Implement a sanity check to detect and recover from confused
0289          * 'nonsense' answers.
0290          */
0291         auto v = QDateTime::fromMSecsSinceEpoch(offset);
0292         if (v.toMSecsSinceEpoch() != offset) {
0293             qCDebug(logger)
0294                 << "Unable to compute datetime matching the given count of time steps:"
0295                 << "Internal confusion in QDateTime detected, number of milliseconds is probably out of range";
0296             return std::nullopt;
0297         }
0298 
0299         return std::optional<QDateTime>(v);
0300     }
0301 }