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 ¶m, 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 }