Line data Source code
1 : // SPDX-FileCopyrightText: 2018 Anne Jan Brouwer
2 : // SPDX-License-Identifier: GPL-3.0-or-later
3 : /**
4 : * @file
5 : * @brief Parser for a decrypted password entry: password line, `key: value`
6 : * fields, remaining text and the OTP configuration.
7 : */
8 : #ifndef SRC_FILECONTENT_H_
9 : #define SRC_FILECONTENT_H_
10 :
11 : #include <QList>
12 : #include <QString>
13 : #include <QStringList>
14 : #include <functional>
15 :
16 : /**
17 : * @struct NamedValue
18 : * @ingroup store
19 : * @brief A name/value pair parsed from a password file field.
20 : */
21 : struct NamedValue {
22 : /**
23 : * @brief Field name (the part before the colon in "name: value").
24 : */
25 : QString name;
26 : /**
27 : * @brief Field value (the part after the colon in "name: value").
28 : */
29 : QString value;
30 : /**
31 : * @brief The field's verbatim source line, colon and spacing intact.
32 : *
33 : * The edit dialog writes an untouched field back from this, so saving
34 : * does not reformat `key:value` to `key: value`, trim the value, or drop
35 : * a field whose value is empty. Empty when the field has no source line
36 : * (a template field the file did not fill).
37 : */
38 : QString originalLine;
39 : };
40 :
41 : /**
42 : * @brief Two fields are equal when name and value match; originalLine is
43 : * formatting, not content, and is left out so tests can compare against
44 : * literals.
45 : * @param a One field.
46 : * @param b The other field.
47 : * @return true when both name and value are equal.
48 : */
49 : inline bool operator==(const NamedValue &a, const NamedValue &b) {
50 : return a.name == b.name && a.value == b.value;
51 : }
52 :
53 : /**
54 : * @ingroup store
55 : * @brief The NamedValues class is mostly a list of NamedValue
56 : * but also has a method to take a specific NamedValue pair out of the list.
57 : */
58 360 : class NamedValues : public QList<NamedValue> {
59 : public:
60 : /** @brief Construct an empty list. */
61 : NamedValues();
62 : /**
63 : * @brief Construct a NamedValues list from an initializer list.
64 : * @param values Initial set of NamedValue pairs.
65 : */
66 : NamedValues(std::initializer_list<NamedValue> values);
67 :
68 : /**
69 : * @brief Remove and return the value for the named field.
70 : * @param name Field name to look up and remove.
71 : * @return The value of the removed field, or an empty string if not found.
72 : */
73 : auto takeValue(const QString &name) -> QString;
74 :
75 : /**
76 : * @brief Remove and return the named field, its original line included.
77 : * @param name Field name to look up and remove.
78 : * @return The removed NamedValue, or a default-constructed one (empty name)
79 : * if not found.
80 : */
81 : auto takeNamedValue(const QString &name) -> NamedValue;
82 : };
83 :
84 : /**
85 : * @class FileContent
86 : * @ingroup store
87 : * @brief Represents the parsed contents of a password file.
88 : *
89 : * Splits the file into a password line, named key/value fields, and
90 : * remaining data. Use FileContent::parse to construct an instance.
91 : */
92 : class FileContent {
93 : public:
94 : /**
95 : * @brief Most named fields parse() takes from one entry.
96 : *
97 : * Every field becomes a row of widgets in the display panel. An entry is
98 : * compressed before it is encrypted, so a small one in a shared store
99 : * could decompress into millions of "a: b" lines within the plaintext
100 : * limit; lines past this many fields stay in the remaining text. They are
101 : * still looked at for an OTP configuration, as fields would be.
102 : */
103 : static constexpr int kMaxNamedFields = 1000;
104 :
105 : /**
106 : * @brief Most lines of remaining text getRemainingDataForDisplay() gives.
107 : *
108 : * The text browser makes a block, with its layout, per line. Past this
109 : * many the display text ends with one line saying how many more there
110 : * are; getRemainingData() keeps them all, so saving loses nothing. The
111 : * edit dialog uses the same number for the whole entry (password and
112 : * fields included) and does not load one with more lines.
113 : */
114 : static constexpr int kMaxDisplayLines = 10000;
115 :
116 : /**
117 : * @brief @p text cut to kMaxDisplayLines lines, as
118 : * getRemainingDataForDisplay() is: for showing an entry as it is
119 : * (displayAsIs).
120 : * @param text The text to show.
121 : * @return @p text when it is short enough, else its first kMaxDisplayLines
122 : * lines and one saying how many more there are.
123 : */
124 : [[nodiscard]] static auto boundedForDisplay(const QString &text) -> QString;
125 :
126 : /**
127 : * @brief parse parses the given fileContent in a FileContent object.
128 : * The password is accessible through getPassword.
129 : * The named value pairs (name: value) are parsed and depending on the
130 : * templateFields and allFields parameters accessible through getNamedValues,
131 : * getRemainingData or getRemainingDataForDisplay.
132 : *
133 : * @param fileContent the file content to parse.
134 : *
135 : * @param templateFields the fields in the template.
136 : * Fields in the template will always be in getNamedValues() at the beginning
137 : * of the list in the same order.
138 : *
139 : * @param allFields whether all fields should be considered as named values.
140 : * If set to false only templateFields are returned in getNamedValues().
141 : *
142 : * @param alsoField a field besides those, by its trimmed name: a secret
143 : * the panel hides, which must not be left in the remaining text in plain
144 : * view. Empty: none.
145 : *
146 : * @return A FileContent instance with password, named values, and remaining
147 : * data populated from the parsed input.
148 : */
149 : static auto parse(const QString &fileContent,
150 : const QStringList &templateFields, bool allFields,
151 : const std::function<bool(const QString &)> &alsoField = {})
152 : -> FileContent;
153 :
154 : /**
155 : * @return the password from the parsed file.
156 : */
157 : [[nodiscard]] auto getPassword() const -> QString;
158 :
159 : /**
160 : * @brief Like getPassword but empty when the password line is itself an
161 : * otpauth URI.
162 : *
163 : * `pass otp insert` writes the URI as the entry's only line, so it lands in
164 : * the password position. It is a shared secret, not a password: it must never
165 : * be rendered or placed on the clipboard. getPassword() still returns it so
166 : * the edit dialog round-trips the file unchanged.
167 : * @return The password, or an empty string when it is an otpauth URI.
168 : */
169 : [[nodiscard]] auto getPasswordForDisplay() const -> QString;
170 :
171 : /**
172 : * @return the named values in the file in the order of appearance, with
173 : * template values first.
174 : */
175 : [[nodiscard]] auto getNamedValues() const -> NamedValues;
176 :
177 : /**
178 : * @return the data that is not the password and not in getNamedValues.
179 : */
180 : [[nodiscard]] auto getRemainingData() const -> QString;
181 :
182 : /**
183 : * @brief Like getRemainingData but without data that should not be displayed
184 : * (like a TOTP secret).
185 : * @return Remaining data string safe for display.
186 : */
187 : [[nodiscard]] auto getRemainingDataForDisplay() const -> QString;
188 :
189 : /**
190 : * @brief How many lines getRemainingData() holds.
191 : * @return The line count; 0 when there is no remaining text.
192 : */
193 : [[nodiscard]] auto getRemainingLineCount() const -> qsizetype;
194 :
195 : /**
196 : * @brief Whether a field name designates the one-time password field.
197 : * @param name Field name as written in the file; surrounding whitespace is
198 : * ignored.
199 : * @return true for "OTP" or "TOTP", compared case-insensitively.
200 : */
201 : [[nodiscard]] static auto isOtpFieldName(const QString &name) -> bool;
202 :
203 : /**
204 : * @brief Whether a field name names a secret: one of @p secrets occurs in
205 : * it as whole words, compared case-insensitively ("key" matches "API key"
206 : * and "Recovery key", not "keyboard layout").
207 : * @param name Field name as written in the file.
208 : * @param secrets The secret field names (AppSettings::secretFieldNames).
209 : * @return true when one of them occurs in @p name.
210 : */
211 : [[nodiscard]] static auto isSecretFieldName(const QString &name,
212 : const QStringList &secrets)
213 : -> bool;
214 :
215 : /**
216 : * @brief Whether a field value is an otpauth URI, and therefore a secret.
217 : *
218 : * A URI is a shared secret whatever the field is called, so both the
219 : * display-suppression and the OTP lookup key off the value as well as the
220 : * name; suppressing only `OTP:`/`TOTP:` leaked `2fa:` and friends.
221 : * @param value Field value; surrounding whitespace is ignored.
222 : * @return true when it starts with "otpauth://", compared case-insensitively.
223 : */
224 : [[nodiscard]] static auto isOtpUriValue(const QString &value) -> bool;
225 :
226 : /**
227 : * @brief The entry's raw one-time password configuration, if it has one.
228 : *
229 : * Looked up in this order:
230 : * 1. the value of the first field whose name satisfies isOtpFieldName();
231 : * 2. the first bare `otpauth://` line in the body, which is how the
232 : * `pass-otp` extension stores it;
233 : * 3. the value of the first `OTP:` line that was not promoted to a named
234 : * value, which is what happens when the template does not list `OTP`.
235 : *
236 : * The string is returned verbatim and is not validated here: it may be an
237 : * `otpauth://` URI or a bare base32 secret. Totp::parse() does the
238 : * validation.
239 : * @return The OTP configuration string, or an empty string when the entry
240 : * has none.
241 : */
242 : [[nodiscard]] auto getOtpUri() const -> QString;
243 :
244 : private:
245 : /**
246 : * @brief Assemble a parsed entry; only parse() calls this.
247 : * @param password The first line.
248 : * @param namedValues The fields promoted out of the body.
249 : * @param remainingData The body lines that did not become fields.
250 : * @param remainingDataDisplay @p remainingData without secret-bearing
251 : * lines, cut at kMaxDisplayLines.
252 : * @param otpUri The OTP configuration found, or empty.
253 : * @param remainingLines Line count of @p remainingData.
254 : */
255 : FileContent(QString password, NamedValues namedValues, QString remainingData,
256 : QString remainingDataDisplay, QString otpUri,
257 : qsizetype remainingLines);
258 :
259 : /** @brief The first line of the entry, verbatim. */
260 : QString password;
261 : /** @brief Fields in order of appearance, template fields first. */
262 : NamedValues namedValues;
263 : /** @brief Body lines that are not fields, joined with newlines; complete. */
264 : QString remainingData, remainingDataDisplay; /**< remainingData for the
265 : panel: OTP-bearing lines and hidden secrets left out, at most
266 : kMaxDisplayLines lines plus a "more" line. */
267 : /** @brief Raw OTP configuration (URI or bare base32), or empty. */
268 : QString otpUri;
269 : /** @brief Number of lines in remainingData. */
270 : qsizetype remainingLines = 0;
271 : };
272 :
273 : #endif // SRC_FILECONTENT_H_
|