File indexing completed on 2024-05-05 05:53:15

0001 /*
0002  * SPDX-License-Identifier: GPL-3.0-or-later
0003  * SPDX-FileCopyrightText: 2020 Johan Ouwerkerk <jm.ouwerkerk@gmail.com>
0004  */
0005 #include "uri.h"
0006 
0007 #include "../base32/base32.h"
0008 #include "../logging_p.h"
0009 
0010 #include <QGlobalStatic>
0011 #include <QScopedPointer>
0012 #include <QTextCodec>
0013 
0014 KEYSMITH_LOGGER(logger, ".uri")
0015 
0016 namespace uri
0017 {
0018     static bool isHexDigit(const char digit)
0019     {
0020         return (digit >= '0' && digit <= '9') || (digit >= 'A' && digit <= 'F') || (digit >= 'a' && digit <= 'f');
0021     }
0022 
0023     std::optional<QByteArray> fromPercentEncoding(const QByteArray &encoded)
0024     {
0025         QByteArray decoded(encoded);
0026 
0027         int index = 0;
0028         for(index = decoded.indexOf('%', index); index >= 0; index = decoded.indexOf('%', index + 1)) {
0029             if (decoded.size() <= (index + 2) || !isHexDigit(decoded[index + 1]) || !isHexDigit(decoded[index + 2])) {
0030                 return std::nullopt;
0031             }
0032 
0033             QByteArray substitute = QByteArray::fromHex(decoded.mid(index + 1, 2));
0034             decoded.replace(index, 3, substitute);
0035         }
0036 
0037         return std::optional<QByteArray>(decoded);
0038     }
0039 
0040     static std::optional<QString> convertUtf8(const QByteArray &data)
0041     {
0042         static QTextCodec *codec = QTextCodec::codecForName("UTF-8");
0043         if (!codec) {
0044             qCDebug(logger) << "Unable to decode data: unable to retrieve codec for UTF-8";
0045             return std::nullopt;
0046         }
0047 
0048         QTextCodec::ConverterState state;
0049         QString result = codec->toUnicode(data.constData(), data.size(), &state);
0050         return state.invalidChars == 0 && state.remainingChars == 0 ? std::optional<QString>(result) : std::nullopt;
0051     }
0052 
0053     std::optional<QString> decodePercentEncoding(const QByteArray &utf8Data)
0054     {
0055         const auto decoded = fromPercentEncoding(utf8Data);
0056         return decoded ? convertUtf8(*decoded) : std::nullopt;
0057     }
0058 
0059     static bool tryDecodeParam(const QByteArray &param, const QByteArray &actual, const QByteArray &value, QByteArray &uri, QString &oldValue, bool &error)
0060     {
0061         bool skipped = true;
0062         if (error || actual != param) {
0063             return skipped;
0064         }
0065 
0066         uri.remove(0, value.size());
0067         skipped = false;
0068 
0069         if (!oldValue.isNull()) {
0070             qCDebug(logger) << "Found duplicate parameter" << param;
0071             error = true;
0072             return skipped;
0073         }
0074 
0075         const auto result = decodePercentEncoding(value);
0076         if (!result) {
0077             qCDebug(logger) << "Failed to decode" << param << "Invalid URI encoding or malformed UTF-8";
0078             error = true;
0079             return skipped;
0080         }
0081 
0082         oldValue = *result;
0083         return skipped;
0084     }
0085 
0086     std::optional<QrParts> QrParts::parse(const QByteArray &qrCode)
0087     {
0088         static const QByteArray schemePrefix("otpauth://");
0089         static const QByteArray totpType("totp");
0090         static const QByteArray hotpType("hotp");
0091         static const QByteArray issuerParam("issuer");
0092         static const QByteArray secretParam("secret");
0093         static const QByteArray algorithmParam("algorithm");
0094         static const QByteArray tokenLengthParam("digits");
0095         static const QByteArray timeStepParam("period");
0096         static const QByteArray counterParam("counter");
0097 
0098         QByteArray uri(qrCode);
0099         if (!uri.startsWith(schemePrefix)) {
0100             qCDebug(logger) << "Unexpected format: URI does not start with:" << schemePrefix;
0101             return std::nullopt;
0102         }
0103 
0104         uri.remove(0, schemePrefix.size());
0105         if (uri.size() < 4) {
0106             qCDebug(logger) << "No token type found: URI too short";
0107             return std::nullopt;
0108         }
0109 
0110         QByteArray typeField = uri.mid(0, 4);
0111         if (typeField != totpType && typeField != hotpType) {
0112             qCDebug(logger) << "Invalid token type found";
0113             return std::nullopt;
0114         }
0115 
0116         Type type = typeField == totpType ? Type::Totp : Type::Hotp;
0117 
0118         uri.remove(0, 4);
0119         int paramOffset = uri.indexOf('?');
0120 
0121         if (paramOffset < 0) {
0122             qCDebug(logger) << "No token parameters found: URI too short";
0123             return std::nullopt;
0124         }
0125 
0126         QString issuer;
0127         QString name(QLatin1String(""));
0128 
0129         if (uri[0] == '/') {
0130             QByteArray issuerNameField = uri.mid(1, paramOffset - 1);
0131 
0132             int colonOffset = issuerNameField.indexOf(':');
0133             int encodedColonOffset = issuerNameField.indexOf(QByteArray("%3A"));
0134 
0135             QByteArray issuerField;
0136             QByteArray nameField = issuerNameField;
0137 
0138             if (colonOffset >= 0 || encodedColonOffset >= 0) {
0139                 if (colonOffset >= 0 && (colonOffset < encodedColonOffset || encodedColonOffset < 0)) {
0140                     issuerField = issuerNameField.mid(0, colonOffset);
0141                     nameField = issuerNameField.mid(colonOffset + 1);
0142                 } else {
0143                     issuerField = issuerNameField.mid(0, encodedColonOffset);
0144                     nameField = issuerNameField.mid(encodedColonOffset + 3);
0145                 }
0146 
0147                 const auto decodedIssuer = uri::decodePercentEncoding(issuerField);
0148                 if (!decodedIssuer) {
0149                     qCDebug(logger) << "Failed to decode issuer: invalid URI encoding or malformed UTF-8";
0150                     return std::nullopt;
0151                 }
0152 
0153                 issuer = *decodedIssuer;
0154             }
0155 
0156             const auto decodedName = uri::decodePercentEncoding(nameField);
0157             if (!decodedName) {
0158                 qCDebug(logger) << "Failed to decode name: invalid URI encoding or malformed UTF-8";
0159                 return std::nullopt;
0160             }
0161 
0162             name = *decodedName;
0163 
0164             uri.remove(0, paramOffset);
0165         }
0166 
0167         if (uri[0] != '?') {
0168             qCDebug(logger) << "No token parameters found: expected to find:" << '?';
0169             return std::nullopt;
0170         }
0171 
0172         QString secret;
0173         QString counter;
0174         QString timeStep;
0175         QString algorithm;
0176         QString tokenLength;
0177         QString otherIssuer;
0178         while (uri.size() > 1) {
0179             uri.remove(0, 1);
0180             QByteArray param;
0181             int valueOffset = uri.indexOf('=');
0182             switch (valueOffset) {
0183             case -1:
0184                 qCDebug(logger) << "No parameter value found: URI too short";
0185                 return std::nullopt;
0186             case 0:
0187                 qCDebug(logger) << "Found a parameter value without a name";
0188                 return std::nullopt;
0189             default:
0190                 param = uri.mid(0, valueOffset);
0191                 uri.remove(0, valueOffset + 1);
0192                 break;
0193             }
0194 
0195             bool error = false;
0196             int nextKeyOffset = uri.indexOf('&');
0197             QByteArray value = uri.mid(0, nextKeyOffset);
0198             if (tryDecodeParam(secretParam, param, value, uri, secret, error) &&
0199                 tryDecodeParam(issuerParam, param, value, uri, otherIssuer, error) &&
0200                 tryDecodeParam(tokenLengthParam, param, value, uri, tokenLength, error) &&
0201                 tryDecodeParam(timeStepParam, param, value, uri, timeStep, error) &&
0202                 tryDecodeParam(counterParam, param, value, uri, counter, error) &&
0203                 tryDecodeParam(algorithmParam, param, value, uri, algorithm, error)) {
0204                 qCDebug(logger) << "Invalid/unsupported parameter found";
0205                 return std::nullopt;
0206             }
0207 
0208             if (error) {
0209                 return std::nullopt;
0210             }
0211         }
0212 
0213         if (secret.isEmpty()) {
0214             qCDebug(logger) << "No token secret found: expected to find:" << secretParam << "parameter";
0215             return std::nullopt;
0216         }
0217 
0218         return std::optional<QrParts>(QrParts(
0219             type,
0220             name,
0221             issuer.isNull() || (issuer.isEmpty() && !otherIssuer.isEmpty()) ? otherIssuer : issuer,
0222             secret,
0223             tokenLength,
0224             counter,
0225             timeStep,
0226             algorithm
0227         ));
0228     }
0229 
0230     std::optional<QrParts> QrParts::parse(const QString &qrCode)
0231     {
0232         return parse(qrCode.toUtf8());
0233     }
0234 
0235     QrParts::Type QrParts::type(void) const
0236     {
0237         return m_type;
0238     }
0239 
0240     QString QrParts::algorithm(void) const
0241     {
0242         return m_algorithm;
0243     }
0244 
0245     QString QrParts::timeStep(void) const
0246     {
0247         return m_timeStep;
0248     }
0249 
0250     QString QrParts::tokenLength(void) const
0251     {
0252         return m_tokenLength;
0253     }
0254 
0255     QString QrParts::counter(void) const
0256     {
0257         return m_counter;
0258     }
0259 
0260     QString QrParts::secret(void) const
0261     {
0262         return m_secret;
0263     }
0264 
0265     QString QrParts::name(void) const
0266     {
0267         return m_name;
0268     }
0269 
0270     QString QrParts::issuer(void) const
0271     {
0272         return m_issuer;
0273     }
0274 
0275     QrParts::QrParts(Type type, const QString &name, const QString &issuer, const QString &secret,
0276                                  const QString &tokenLength, const QString &counter, const QString &timeStep,
0277                                  const QString &algorithm) : //, const Warnings &warnings) :
0278         m_type(type), m_name(name), m_issuer(issuer), m_secret(secret), m_tokenLength(tokenLength),
0279         m_counter(counter), m_timeStep(timeStep), m_algorithm(algorithm) //, m_warnings(warnings)
0280     {
0281     }
0282 }