QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
filecontent.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2018 Anne Jan Brouwer
2// SPDX-License-Identifier: GPL-3.0-or-later
9#include "filecontent.h"
10#include <QRegularExpression>
11
12#include <QCoreApplication>
13#include <QStringTokenizer>
14
15#include <algorithm>
16#include <limits>
17
18#include <utility>
19
25static auto isOtpUri(const QString &value) -> bool {
26 return FileContent::isOtpUriValue(value);
27}
28
37static auto isLineHidden(const QString &line) -> bool {
38 if (isOtpUri(line)) {
39 return true;
40 }
41 const qsizetype colon = line.indexOf(':');
42 if (colon < 0) {
43 return false;
44 }
45 return FileContent::isOtpFieldName(line.left(colon)) ||
46 isOtpUri(line.right(line.length() - colon - 1));
47}
48
49namespace {
50
56struct OtpCandidates {
57 QString field;
58 QString bareLine;
59 QString remainingField;
60
65 auto pick() const -> QString {
66 if (!field.isEmpty()) {
67 return field;
68 }
69 return bareLine.isEmpty() ? remainingField : bareLine;
70 }
71};
72
85auto takeNamedValue(const QString &line, const QStringList &templateFields,
86 bool allFields,
87 const std::function<bool(const QString &)> &alsoField,
88 NamedValues &namedValues, OtpCandidates &otp) -> bool {
89 const qsizetype colon = line.indexOf(u':');
90 if (colon < 0) {
91 return false;
92 }
93 const QString name = line.left(colon);
94 const QString value = line.right(line.length() - colon - 1);
95 // A value starting with // means the colon is probably from a URL.
96 if (!templateFields.contains(name) &&
97 !(allFields && !value.startsWith(QStringLiteral("//"))) &&
98 !(alsoField && alsoField(name.trimmed()))) {
99 // An `OTP:` line the template did not promote to a field.
100 if (otp.remainingField.isEmpty() && FileContent::isOtpFieldName(name)) {
101 otp.remainingField = value.trimmed();
102 }
103 return false;
104 }
105 namedValues.append({name.trimmed(), value.trimmed(), line});
106 // Keyed on the value as well as the name: a field called anything at all
107 // whose value is an otpauth URI is still a shared secret.
108 if (otp.field.isEmpty() && (FileContent::isOtpFieldName(name) ||
110 otp.field = value.trimmed();
111 }
112 return true;
113}
114
120auto moreLinesNotShown(qsizetype count) -> QString {
121 // The count in a variable of its own: lupdate up to 6.8 loses a
122 // translate() call with template arguments among its arguments and would
123 // mark the string vanished.
124 const int n = static_cast<int>(
125 std::min<qsizetype>(count, std::numeric_limits<int>::max()));
126 return QCoreApplication::translate("FileContent",
127 "… %n more line(s) not shown", nullptr, n);
128}
129
130} // namespace
131
132auto FileContent::parse(const QString &fileContent,
133 const QStringList &templateFields, bool allFields,
134 const std::function<bool(const QString &)> &alsoField)
135 -> FileContent {
136 // Line by line over the text, never a list of every line: a compressed
137 // entry can expand into tens of millions of lines within the plaintext
138 // limit, and a QString each (empty ones too) would take gigabytes.
139 QStringTokenizer tokens = QStringView(fileContent).tokenize(u'\n');
140 auto it = tokens.begin();
141 QString password;
142 if (it != tokens.end()) {
143 password = it->toString();
144 ++it;
145 }
146 QString remainingData;
147 QString remainingDataDisplay;
148 qsizetype remainingLines = 0;
149 qsizetype displayedLines = 0;
150 qsizetype notShown = 0;
151 // Past kMaxNamedFields a line is still offered to takeNamedValue(), into
152 // this throwaway list, for what it notes about OTP: the line stays text.
153 NamedValues pastTheCap;
155 OtpCandidates otp;
156 // An entry from `pass otp insert` has the URI as its only (password) line,
157 // which the loop below never sees.
158 if (isOtpUri(password)) {
159 otp.bareLine = password.trimmed();
160 }
161 for (; it != tokens.end(); ++it) {
162 const QString line = it->toString();
163 bool secretPastTheCap = false;
164 // A bare otpauth:// line is never a field, also not when the template
165 // lists "otpauth": split at its colon, the value would lose the scheme
166 // that marks it as a shared secret and be shown seed and all. Past
167 // kMaxNamedFields, lines stay text.
168 if (!isOtpUri(line)) {
169 if (namedValues.size() < kMaxNamedFields) {
170 if (takeNamedValue(line, templateFields, allFields, alsoField,
171 namedValues, otp)) {
172 continue;
173 }
174 } else {
175 takeNamedValue(line, templateFields, allFields, alsoField, pastTheCap,
176 otp);
177 // A secret past the cap stays text, but not on screen: the panel
178 // would have hidden it.
179 secretPastTheCap = !pastTheCap.isEmpty() && alsoField &&
180 alsoField(pastTheCap.first().name);
181 pastTheCap.clear();
182 }
183 }
184 // Joined with "\n" as they go, as split() and join() did.
185 if (remainingLines > 0) {
186 remainingData += QLatin1Char('\n');
187 }
188 remainingData += line;
190 if (isOtpUri(line) && otp.bareLine.isEmpty()) {
191 // A bare otpauth:// line, the convention the pass-otp extension uses.
192 otp.bareLine = line.trimmed();
193 }
194 if (!isLineHidden(line) && !secretPastTheCap) {
195 if (displayedLines == kMaxDisplayLines) {
196 ++notShown;
197 continue;
198 }
199 if (displayedLines > 0) {
200 remainingDataDisplay += QLatin1Char('\n');
201 }
202 remainingDataDisplay += line;
203 ++displayedLines;
204 }
205 }
206 if (notShown > 0) {
207 remainingDataDisplay += QLatin1Char('\n') + moreLinesNotShown(notShown);
208 }
209 // The OTP value stays in namedValues/remainingData: the edit dialog reads it
210 // from there and PasswordDialog::getPassword() drops empty fields, so
211 // stripping it here would delete the secret on the next save. Suppression
212 // happens in getRemainingDataForDisplay() and PasswordDisplayPanel.
214 otp.pick(), remainingLines};
215}
216
217auto FileContent::isOtpFieldName(const QString &name) -> bool {
218 const QString trimmed = name.trimmed();
219 return trimmed.compare(QStringLiteral("OTP"), Qt::CaseInsensitive) == 0 ||
220 trimmed.compare(QStringLiteral("TOTP"), Qt::CaseInsensitive) == 0;
221}
222
223auto FileContent::isSecretFieldName(const QString &name,
224 const QStringList &secrets) -> bool {
225 // One pattern for the whole list, compiled once per list: the panel asks
226 // for every field of every entry shown, and the list rarely changes.
227 thread_local QStringList cachedSecrets;
228 thread_local QStringList cachedWords;
229 thread_local QRegularExpression cachedPattern;
230 thread_local bool cachedYet = false;
231 constexpr auto options = QRegularExpression::CaseInsensitiveOption |
232 QRegularExpression::UseUnicodePropertiesOption;
233 const auto whole = [](const QString &alternatives) {
234 return QStringLiteral("(?<!\\w)(?:%1)(?!\\w)").arg(alternatives);
235 };
236 if (!cachedYet || secrets != cachedSecrets) {
237 cachedWords.clear();
238 for (const QString &secret : secrets) {
239 const QString word = secret.trimmed();
240 if (!word.isEmpty()) {
241 cachedWords << QRegularExpression::escape(word);
242 }
243 }
244 cachedSecrets = secrets;
245 cachedYet = true;
246 cachedPattern =
247 QRegularExpression(whole(cachedWords.join(QLatin1Char('|'))), options);
248 }
249 if (cachedWords.isEmpty()) {
250 return false;
251 }
252 if (cachedPattern.isValid()) {
253 return cachedPattern.match(name).hasMatch();
254 }
255 // Too many words for one pattern (PCRE2's size limit): word by word,
256 // rather than every secret field shown in plain view.
257 return std::any_of(
258 cachedWords.cbegin(), cachedWords.cend(),
259 [&name, &whole, options](const QString &word) {
260 return QRegularExpression(whole(word), options).match(name).hasMatch();
261 });
262}
263
264auto FileContent::isOtpUriValue(const QString &value) -> bool {
265 return value.trimmed().startsWith(QStringLiteral("otpauth://"),
266 Qt::CaseInsensitive);
267}
268
269auto FileContent::getPassword() const -> QString { return this->password; }
270
271auto FileContent::getPasswordForDisplay() const -> QString {
272 return isOtpUri(this->password) ? QString() : this->password;
273}
274
276 return this->namedValues;
277}
278
279auto FileContent::getRemainingData() const -> QString {
280 return this->remainingData;
281}
282
284 return this->remainingDataDisplay;
285}
286
287auto FileContent::getOtpUri() const -> QString { return this->otpUri; }
288
296
297auto FileContent::boundedForDisplay(const QString &text) -> QString {
298 // Where the line after the last one shown starts, if there is one.
299 qsizetype end = -1;
300 for (int line = 0; line < kMaxDisplayLines; ++line) {
301 end = text.indexOf(QLatin1Char('\n'), end + 1);
302 if (end < 0) {
303 return text;
304 }
305 }
306 const qsizetype more =
307 text.count(QLatin1Char('\n'), Qt::CaseSensitive) - (kMaxDisplayLines - 1);
308 return text.left(end) + QLatin1Char('\n') + moreLinesNotShown(more);
309}
310
311auto FileContent::getRemainingLineCount() const -> qsizetype {
312 return remainingLines;
313}
314
315NamedValues::NamedValues() = default;
316
317NamedValues::NamedValues(std::initializer_list<NamedValue> values)
318 : QList(values) {}
319
320auto NamedValues::takeValue(const QString &name) -> QString {
321 return takeNamedValue(name).value;
322}
323
324auto NamedValues::takeNamedValue(const QString &name) -> NamedValue {
325 for (int i = 0; i < length(); ++i) {
326 if (at(i).name == name) {
327 return takeAt(i);
328 }
329 }
330 return {};
331}
static auto boundedForDisplay(const QString &text) -> QString
text cut to kMaxDisplayLines lines, as getRemainingDataForDisplay() is: for showing an entry as it is...
FileContent(QString password, NamedValues namedValues, QString remainingData, QString remainingDataDisplay, QString otpUri, qsizetype remainingLines)
Assemble a parsed entry; only parse() calls this.
qsizetype remainingLines
Number of lines in remainingData.
QString remainingData
Body lines that are not fields, joined with newlines; complete.
auto getRemainingLineCount() const -> qsizetype
How many lines getRemainingData() holds.
QString password
The first line of the entry, verbatim.
static auto isOtpFieldName(const QString &name) -> bool
Whether a field name designates the one-time password field.
QString otpUri
Raw OTP configuration (URI or bare base32), or empty.
static constexpr int kMaxDisplayLines
Most lines of remaining text getRemainingDataForDisplay() gives.
auto getRemainingData() const -> QString
auto getNamedValues() const -> NamedValues
static constexpr int kMaxNamedFields
Most named fields parse() takes from one entry.
static auto parse(const QString &fileContent, const QStringList &templateFields, bool allFields, const std::function< bool(const QString &)> &alsoField={}) -> FileContent
parse parses the given fileContent in a FileContent object. The password is accessible through getPas...
static auto isSecretFieldName(const QString &name, const QStringList &secrets) -> bool
Whether a field name names a secret: one of secrets occurs in it as whole words, compared case-insens...
NamedValues namedValues
Fields in order of appearance, template fields first.
auto getRemainingDataForDisplay() const -> QString
Like getRemainingData but without data that should not be displayed (like a TOTP secret).
auto getPassword() const -> QString
auto getOtpUri() const -> QString
The entry's raw one-time password configuration, if it has one.
QString remainingDataDisplay
auto getPasswordForDisplay() const -> QString
Like getPassword but empty when the password line is itself an otpauth URI.
static auto isOtpUriValue(const QString &value) -> bool
Whether a field value is an otpauth URI, and therefore a secret.
The NamedValues class is mostly a list of NamedValue but also has a method to take a specific NamedVa...
Definition filecontent.h:58
auto takeValue(const QString &name) -> QString
Remove and return the value for the named field.
auto takeNamedValue(const QString &name) -> NamedValue
Remove and return the named field, its original line included.
NamedValues()
Construct an empty list.
static auto isLineHidden(const QString &line) -> bool
Whether a body line carries a TOTP shared secret and must stay out of the parsed display: a bare otpa...
static auto isOtpUri(const QString &value) -> bool
Shorthand for the public FileContent::isOtpUriValue.
Parser for a decrypted password entry: password line, key: value fields, remaining text and the OTP c...
A name/value pair parsed from a password file field.
Definition filecontent.h:21