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 }