QtPass 2.0.0
Developer documentation
Loading...
Searching...
No Matches
base32.cpp
Go to the documentation of this file.
1// SPDX-FileCopyrightText: 2025 KeePassXC Team <team@keepassxc.org>
2// SPDX-FileCopyrightText: 2026 Anne Jan Brouwer
3// SPDX-License-Identifier: GPL-3.0-or-later
4
16
17#include "base32.h"
18
19#include <optional>
20
21namespace {
22
30constexpr quint64 MASK_40BIT = quint64(0xF8) << 32;
31constexpr quint64 MASK_35BIT = quint64(0x7C0000000);
32constexpr quint64 MASK_25BIT = quint64(0x1F00000);
33constexpr quint64 MASK_20BIT = quint64(0xF8000);
34constexpr quint64 MASK_10BIT = quint64(0x3E0);
36
38constexpr char kAlphabet[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
40constexpr quint8 ALPH_POS_2 = 26;
41
43constexpr quint8 ASCII_2 = static_cast<quint8>('2');
45constexpr quint8 ASCII_7 = static_cast<quint8>('7');
47constexpr quint8 ASCII_A = static_cast<quint8>('A');
49constexpr quint8 ASCII_Z = static_cast<quint8>('Z');
51constexpr quint8 ASCII_a = static_cast<quint8>('a');
53constexpr quint8 ASCII_z = static_cast<quint8>('z');
55constexpr quint8 ASCII_EQ = static_cast<quint8>('=');
56
66auto countPadding(const QByteArray &encodedData) -> int {
67 int nPads = 0;
68 for (qsizetype i = encodedData.size() - 1;
69 i >= 0 && i > encodedData.size() - 7; --i) {
70 if (encodedData.at(i) != '=') {
71 break;
72 }
73 ++nPads;
74 }
75 return nPads;
76}
77
85struct Tail {
86 int bytes = 0;
87 int offset = 0;
88};
89
92auto tailShape(int nPads) -> std::optional<Tail> {
93 switch (nPads) {
94 case 0:
95 return Tail{};
96 case 1:
97 return Tail{4, 3};
98 case 3:
99 return Tail{3, 1};
100 case 4:
101 return Tail{2, 4};
102 case 6:
103 return Tail{1, 2};
104 default:
105 return std::nullopt;
106 }
107}
108
111auto symbolValue(quint8 ch) -> std::optional<quint8> {
112 if ((ASCII_A <= ch && ch <= ASCII_Z) || (ASCII_a <= ch && ch <= ASCII_z)) {
113 ch -= ASCII_A;
114 if (ch >= ALPH_POS_2) {
115 // Fold lower case onto the same alphabet positions as upper case.
116 ch -= ASCII_a - ASCII_A;
117 }
118 return ch;
119 }
120 if (ASCII_2 <= ch && ch <= ASCII_7) {
121 return static_cast<quint8>(ch - ASCII_2 + ALPH_POS_2);
122 }
123 return std::nullopt;
124}
125
126} // namespace
127
128auto Base32::decode(const QByteArray &encodedData) -> QByteArray {
129 // Strict: a well-formed base32 string is always padded to a multiple of 8.
130 if (encodedData.isEmpty() || encodedData.size() % 8 != 0) {
131 return {};
132 }
133 const int nPads = countPadding(encodedData);
134 const std::optional<Tail> tail = tailShape(nPads);
135 if (!tail) {
136 return {};
137 }
138
139 const qsizetype nQuanta = encodedData.size() / 8;
140 const qsizetype nBytes =
141 tail->bytes > 0 ? (nQuanta - 1) * 5 + tail->bytes : nQuanta * 5;
142
143 QByteArray data(nBytes, Qt::Uninitialized);
144 // Written through a raw pointer to avoid QByteArray's detach check on every
145 // subscript in the hot loop.
146 char *out = data.data();
147
148 qsizetype i = 0;
149 qsizetype o = 0;
150 // Everything from the first '=' onward must be padding. Reuse the pad count
151 // computed above rather than scanning the tail a second time.
152 const qsizetype firstPad = encodedData.size() - nPads;
153
154 while (i < encodedData.size()) {
155 quint64 quantum = 0;
156 int nQuantumBytes = 5;
157
158 for (int n = 0; n < 8; ++n) {
159 const auto ch = static_cast<quint8>(encodedData.at(i++));
160 if (ASCII_EQ == ch) {
161 if (i - 1 < firstPad) {
162 // '=' before the trailing run is not padding, it is malformed input.
163 return {};
164 }
165 if (i == encodedData.size()) {
166 // Finished with the short final quantum.
167 quantum >>= tail->offset;
168 nQuantumBytes = tail->bytes;
169 }
170 continue;
171 }
172 const std::optional<quint8> value = symbolValue(ch);
173 if (!value) {
174 return {};
175 }
176 quantum <<= 5;
177 quantum |= *value;
178 }
179
180 const int offset = (nQuantumBytes - 1) * 8;
181 quint64 mask = quint64(0xFF) << offset;
182 for (int n = offset; n >= 0 && o < nBytes; n -= 8) {
183 out[o++] = static_cast<char>((quantum & mask) >> n);
184 mask >>= 8;
185 }
186 }
187
188 return data;
189}
190
191auto Base32::encode(const QByteArray &data) -> QByteArray {
192 if (data.isEmpty()) {
193 return {};
194 }
195
196 const qsizetype nBits = data.size() * 8;
197 const int rBits = static_cast<int>(nBits % 40); // in {0, 8, 16, 24, 32}
198 const qsizetype nQuanta = nBits / 40 + (rBits > 0 ? 1 : 0);
199 const qsizetype nBytes = nQuanta * 8;
200 QByteArray encodedData(nBytes, Qt::Uninitialized);
201 // Raw pointer: see the note in decode().
202 char *out = encodedData.data();
203
204 qsizetype i = 0;
205 qsizetype o = 0;
206 int n = 0;
207 quint64 mask = 0;
208 quint64 quantum = 0;
209
210 // QByteArray elements are signed char: widening them directly (as KeePassXC
211 // does) sign-extends any byte >= 0x80 and the resulting high bits corrupt
212 // the neighbouring bytes of the quantum. Cast through quint8.
213 const auto byteAt = [&data](qsizetype index) -> quint64 {
214 return static_cast<quint64>(static_cast<quint8>(data.at(index)));
215 };
216
217 // 40 bits of input per input group.
218 while (i + 5 <= data.size()) {
219 quantum = 0;
220 for (n = 32; n >= 0; n -= 8) {
221 quantum |= byteAt(i++) << n;
222 }
223
224 mask = MASK_40BIT;
225 for (n = 35; n >= 0; n -= 5) {
226 out[o++] = kAlphabet[(quantum & mask) >> n];
227 mask >>= 5;
228 }
229 }
230
231 // Fewer than 40 bits of input in the final input group.
232 if (i < data.size()) {
233 quantum = 0;
234 for (n = rBits - 8; n >= 0; n -= 8) {
235 quantum |= byteAt(i++) << n;
236 }
237
238 switch (rBits) {
239 case 8: // expand to 10 bits
240 quantum <<= 2;
241 mask = MASK_10BIT;
242 n = 5;
243 break;
244 case 16: // expand to 20 bits
245 quantum <<= 4;
246 mask = MASK_20BIT;
247 n = 15;
248 break;
249 case 24: // expand to 25 bits
250 quantum <<= 1;
251 mask = MASK_25BIT;
252 n = 20;
253 break;
254 default: // expand to 35 bits
255 quantum <<= 3;
256 mask = MASK_35BIT;
257 n = 30;
258 }
259
260 while (n >= 0) {
261 out[o++] = kAlphabet[(quantum & mask) >> n];
262 mask >>= 5;
263 n -= 5;
264 }
265
266 // Add the pad characters.
267 while (o < encodedData.size()) {
268 out[o++] = '=';
269 }
270 }
271
272 return encodedData;
273}
274
275auto Base32::addPadding(const QByteArray &encodedData) -> QByteArray {
276 if (encodedData.isEmpty() || encodedData.size() % 8 == 0) {
277 return encodedData;
278 }
279
280 const int rBytes = static_cast<int>(encodedData.size() % 8);
281 // rBytes must be a member of {2, 4, 5, 7}.
282 if (1 == rBytes || 3 == rBytes || 6 == rBytes) {
283 return encodedData;
284 }
285
286 QByteArray newEncodedData(encodedData);
287 for (int nPads = 8 - rBytes; nPads > 0; --nPads) {
288 newEncodedData.append('=');
289 }
290
291 return newEncodedData;
292}
293
294auto Base32::removePadding(const QByteArray &encodedData) -> QByteArray {
295 if (encodedData.isEmpty() || encodedData.size() % 8 != 0) {
296 return encodedData; // return the same bad input
297 }
298
299 QByteArray newEncodedData(encodedData);
300 newEncodedData.resize(encodedData.size() - countPadding(encodedData));
301
302 return newEncodedData;
303}
304
305auto Base32::sanitizeInput(const QByteArray &encodedData) -> QByteArray {
306 if (encodedData.isEmpty()) {
307 return encodedData;
308 }
309
310 QByteArray newEncodedData(encodedData.size(), Qt::Uninitialized);
311 // Raw pointer: see the note in decode().
312 char *out = newEncodedData.data();
313 qsizetype i = 0;
314 for (auto ch : encodedData) {
315 switch (ch) {
316 case '0':
317 out[i++] = 'O';
318 break;
319 case '1':
320 out[i++] = 'L';
321 break;
322 case '8':
323 out[i++] = 'B';
324 break;
325 default:
326 if (('A' <= ch && ch <= 'Z') || ('a' <= ch && ch <= 'z') ||
327 ('2' <= ch && ch <= '7')) {
328 out[i++] = ch;
329 }
330 }
331 }
332 newEncodedData.resize(i);
333
334 return addPadding(newEncodedData);
335}
Strict RFC 4648 base32 codec plus the input sanitizer that makes human-typed TOTP secrets decodable.
static auto sanitizeInput(const QByteArray &encodedData) -> QByteArray
Make human-entered base32 text decodable.
Definition base32.cpp:305
static auto addPadding(const QByteArray &encodedData) -> QByteArray
Append '=' padding up to a multiple of 8 characters.
Definition base32.cpp:275
static auto decode(const QByteArray &encodedData) -> QByteArray
Decode strict RFC 4648 base32 data.
Definition base32.cpp:128
static auto encode(const QByteArray &data) -> QByteArray
Encode raw bytes as padded RFC 4648 base32.
Definition base32.cpp:191
static auto removePadding(const QByteArray &encodedData) -> QByteArray
Strip trailing '=' padding.
Definition base32.cpp:294