• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

kimci86 / bkcrack / 12977397671

18 Jan 2025 01:31AM UTC coverage: 76.135% (+3.0%) from 73.141%
12977397671

Pull #126

github

kimci86
Implement mask-based password recovery
Pull Request #126: Mask attack to recover password

313 of 364 new or added lines in 3 files covered. (85.99%)

2 existing lines in 1 file now uncovered.

1560 of 2049 relevant lines covered (76.13%)

1623646.16 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

88.38
/src/password.cpp
1
#include "password.hpp"
2

3
#include "Crc32Tab.hpp"
4
#include "MultTab.hpp"
5
#include "log.hpp"
6

7
#include <algorithm>
8
#include <atomic>
9
#include <bitset>
10
#include <iomanip>
11
#include <mutex>
12
#include <thread>
13

14
template <typename Derived /* must implement onSolutionFound() method */>
15
class SixCharactersRecovery
16
{
17
public:
18
    void setTarget(const Keys& target, const std::vector<std::uint8_t>& charset5)
30,354✔
19
    {
20
        // initialize target X, Y and Z values
21
        x[6] = target.getX();
30,354✔
22
        y[6] = target.getY();
30,358✔
23
        z[6] = target.getZ();
30,346✔
24

25
        // derive Y5
26
        y[5] = (y[6] - 1) * MultTab::multInv - lsb(x[6]);
30,358✔
27

28
        // derive more Z bytes
29
        for (auto i = 6; 1 < i; i--)
181,358✔
30
            z[i - 1] = Crc32Tab::crc32inv(z[i], msb(y[i]));
150,976✔
31

32
        // precompute possible Z0[16,32) and Z{-1}[24,32)
33
        z0_16_32.reset();
30,382✔
34
        zm1_24_32.reset();
30,448✔
35
        for (const auto p5 : charset5)
269,230✔
36
        {
37
            x[5] = Crc32Tab::crc32inv(x[6], p5);
238,514✔
38
            y[4] = (y[5] - 1) * MultTab::multInv - lsb(x[5]);
234,358✔
39
            z[3] = Crc32Tab::crc32inv(z[4], msb(y[4]));
237,374✔
40

41
            const auto y3_ignoring_lsb_x4 = (y[4] - 1) * MultTab::multInv;
236,194✔
42
            const auto msb_y3_min         = msb(y3_ignoring_lsb_x4 - 255);
238,418✔
43
            const auto msb_y3_max         = msb(y3_ignoring_lsb_x4 - 0);
237,248✔
44

45
            z[2] = Crc32Tab::crc32inv(z[3], msb_y3_min);
237,502✔
46
            z[1] = Crc32Tab::crc32inv(z[2], 0);
235,798✔
47
            z[0] = Crc32Tab::crc32inv(z[1], 0);
237,440✔
48

49
            z0_16_32.set(z[0] >> 16);
237,606✔
50
            zm1_24_32.set(Crc32Tab::crc32inv(z[0], 0) >> 24);
239,924✔
51

52
            if (msb_y3_max != msb_y3_min)
238,786✔
53
            {
54
                z[2] = Crc32Tab::crc32inv(z[3], msb_y3_max);
6✔
55
                z[1] = Crc32Tab::crc32inv(z[2], 0);
6✔
56
                z[0] = Crc32Tab::crc32inv(z[1], 0);
6✔
57

58
                z0_16_32.set(z[0] >> 16);
6✔
59
                zm1_24_32.set(Crc32Tab::crc32inv(z[0], 0) >> 24);
6✔
60
            }
61
        }
62
    }
28,388✔
63

64
    void search(const Keys& initial)
136✔
65
    {
66
        // check compatible Z0[16,32)
67
        if (!z0_16_32[initial.getZ() >> 16])
136✔
68
            return;
114✔
69

70
        // initialize starting X, Y and Z values
71
        x[0] = candidateX0 = initial.getX();
22✔
72
        y[0]               = initial.getY();
22✔
73
        z[0]               = initial.getZ();
22✔
74

75
        // complete Z values and derive Y[24,32) values
76
        for (auto i = 1; i <= 4; i++)
110✔
77
        {
78
            y[i] = Crc32Tab::getYi_24_32(z[i], z[i - 1]);
88✔
79
            z[i] = Crc32Tab::crc32(z[i - 1], msb(y[i]));
88✔
80
        }
81

82
        // recursively complete Y values and derive password
83
        searchRecursive(5);
22✔
84
    }
85

86
protected:
87
    void searchRecursive(int i)
227,572✔
88
    {
89
        if (i != 1) // the Y-list is not complete so generate Y{i-1} values
227,572✔
90
        {
91
            const auto fy  = (y[i] - 1) * MultTab::multInv;
180,006✔
92
            const auto ffy = (fy - 1) * MultTab::multInv;
180,060✔
93

94
            // get possible LSB(Xi)
95
            for (const auto xi_0_8 : MultTab::getMsbProdFiber2(msb(ffy - (y[i - 2] & mask<24, 32>))))
553,684✔
96
            {
97
                // compute corresponding Y{i-1}
98
                const auto yim1 = fy - xi_0_8;
372,976✔
99

100
                // filter values with Y{i-2}[24,32)
101
                if (ffy - MultTab::multInv * xi_0_8 - (y[i - 2] & mask<24, 32>) <= maxdiff<24> &&
564,138✔
102
                    msb(yim1) == msb(y[i - 1]))
190,474✔
103
                {
104
                    // add Y{i-1} to the Y-list
105
                    y[i - 1] = yim1;
190,358✔
106

107
                    // set Xi value
108
                    x[i] = xi_0_8;
190,312✔
109

110
                    searchRecursive(i - 1);
190,356✔
111
                }
112
            }
113
        }
114
        else // the Y-list is complete
115
        {
116
            // only the X1 LSB was not set yet, so do it here
117
            x[1] = (y[1] - 1) * MultTab::multInv - y[0];
47,566✔
118
            if (x[1] > 0xff)
47,948✔
119
                return;
47,912✔
120

121
            // complete X values and derive password
122
            for (auto j = 5; 0 <= j; j--)
266✔
123
            {
124
                const auto xi_xor_pi = Crc32Tab::crc32inv(x[j + 1], 0);
228✔
125
                p[j]                 = lsb(xi_xor_pi ^ x[j]);
228✔
126
                x[j]                 = xi_xor_pi ^ p[j];
228✔
127
            }
128

129
            if (x[0] == candidateX0) // the password is successfully recovered
38✔
130
                (static_cast<Derived*>(this))->onSolutionFound();
38✔
131
        }
132
    }
133

134
    // set of possible Z0[16,32) values considering given character set
135
    std::bitset<1 << 16> z0_16_32;
136

137
    // set of possible Z{-1}[24,32) values considering given character set
138
    std::bitset<1 << 8> zm1_24_32;
139

140
    // cipher state (X,Y,Z)_i for index i in [0, 6] where the last state (X,Y,Z)_6 is
141
    // the representation of the password to recover
142
    std::array<std::uint32_t, 7> x{}, y{}, z{};
143
    std::uint32_t                candidateX0{}; // backup of candidate X value for convenience
144

145
    std::array<std::uint8_t, 6> p{}; // password last 6 bytes
146
};
147

148
class BruteforceRecovery : public SixCharactersRecovery<BruteforceRecovery>
149
{
150
public:
151
    BruteforceRecovery(const Keys& keys, const std::vector<std::uint8_t>& charset, std::vector<std::string>& solutions,
9✔
152
                       std::mutex& solutionsMutex, bool exhaustive, Progress& progress)
153
    : charset{charset}
18✔
154
    , solutions{solutions}
9✔
155
    , solutionsMutex{solutionsMutex}
9✔
156
    , exhaustive{exhaustive}
9✔
157
    , progress{progress}
9✔
158
    {
159
        setTarget(keys, charset);
9✔
160
    }
9✔
161

162
    void search(std::size_t length)
63✔
163
    {
164
        auto restart = std::string{};
63✔
165
        search(length, "", restart, 1);
63✔
166
    }
63✔
167

168
    void search(std::size_t length, const std::string& start, std::string& restart, int jobs)
75✔
169
    {
170
        prefix.clear();
75✔
171
        this->length = length;
75✔
172

173
        if (length <= 6)
75✔
174
            searchShort();
63✔
175
        else if (length <= 9)
12✔
176
            searchLongRecursive(Keys{});
8✔
177
        else
178
        {
179
            progress.done  = 0;
4✔
180
            progress.total = charset.size() * charset.size();
4✔
181
            searchLongParallelRecursive(Keys{}, start, restart, jobs);
4✔
182
        }
183
    }
75✔
184

185
    void onSolutionFound() const
9✔
186
    {
187
        auto password = prefix;
9✔
188
        password.append(p.begin(), p.end());
9✔
189
        password.erase(password.begin(), password.end() - length);
9✔
190

191
        const auto isInCharset =
192
            std::all_of(password.begin(), password.end(),
9✔
193
                        [this](char c)
58✔
194
                        { return std::binary_search(charset.begin(), charset.end(), static_cast<std::uint8_t>(c)); });
58✔
195

196
        if (!isInCharset)
9✔
197
        {
198
            progress.log(
×
199
                [&password](std::ostream& os)
×
200
                {
201
                    const auto flagsBefore = os.setf(std::ios::hex, std::ios::basefield);
×
202
                    const auto fillBefore  = os.fill('0');
×
203

204
                    os << "Password: " << password << " (as bytes:";
×
205
                    for (const auto c : password)
×
206
                        os << ' ' << std::setw(2) << int{static_cast<std::uint8_t>(c)};
×
207
                    os << ')' << std::endl;
×
208

209
                    os.fill(fillBefore);
×
210
                    os.flags(flagsBefore);
×
211

212
                    os << "Some characters are not in the expected charset. Continuing." << std::endl;
×
213
                });
×
214

215
            return;
×
216
        }
217

218
        {
219
            const auto lock = std::scoped_lock{solutionsMutex};
9✔
220
            solutions.push_back(password);
9✔
221
        }
9✔
222

223
        progress.log([&password](std::ostream& os) { os << "Password: " << password << std::endl; });
18✔
224

225
        if (!exhaustive)
9✔
226
            progress.state = Progress::State::EarlyExit;
9✔
227
    }
9✔
228

229
private:
230
    /// \brief Look for a password of length 6 or less
231
    ///
232
    /// \pre prefix.empty() && length <= 6
233
    void searchShort()
63✔
234
    {
235
        auto initial = Keys{};
63✔
236
        // update initial state backward so that there are exactly 6 updates between it and the target state
237
        for (auto i = length; i < 6; i++)
252✔
238
            initial.updateBackwardPlaintext('\0');
189✔
239

240
        SixCharactersRecovery::search(initial);
63✔
241
    }
63✔
242

243
    /// \brief Look for password of length 7 or more
244
    ///
245
    /// Recursively iterate on possible prefixes of length-6 characters.
246
    /// For each prefix, try to recover the last 6 characters with SixCharactersRecovery::search.
247
    ///
248
    /// \pre prefix.size() + 6 < length
249
    void searchLongRecursive(const Keys& initial)
677,345✔
250
    {
251
        if (prefix.size() + 7 == length) // there is only one more character to bruteforce
677,345✔
252
        {
253
            // check compatible Z{-1}[24, 32)
254
            if (!zm1_24_32[initial.getZ() >> 24])
659,963✔
255
                return;
573,595✔
256

257
            prefix.push_back(charset[0]);
100,130✔
258

259
            // precompute as much as we can about the next cipher state without knowing the password byte
260
            const auto x0_partial = Crc32Tab::crc32(initial.getX(), 0);
100,110✔
261
            const auto y0_partial = initial.getY() * MultTab::mult + 1;
100,087✔
262
            const auto z0_partial = Crc32Tab::crc32(initial.getZ(), 0);
100,081✔
263

264
            for (const auto pi : charset)
3,241,550✔
265
            {
266
                // finish to update the cipher state
267
                const auto x0 = x0_partial ^ Crc32Tab::crc32(0, pi);
3,218,383✔
268
                const auto y0 = y0_partial + MultTab::mult * lsb(x0);
3,140,588✔
269
                const auto z0 = z0_partial ^ Crc32Tab::crc32(0, msb(y0));
3,145,497✔
270

271
                // SixCharactersRecovery::search is inlined below for performance
272

273
                // check compatible Z0[16,32)
274
                if (!z0_16_32[z0 >> 16])
3,334,917✔
275
                    continue;
3,124,069✔
276

277
                prefix.back() = pi;
17,271✔
278

279
                // initialize starting X, Y and Z values
280
                x[0] = candidateX0 = x0;
17,276✔
281
                y[0]               = y0;
17,269✔
282
                z[0]               = z0;
17,268✔
283

284
                // complete Z values and derive Y[24,32) values
285
                y[1] = Crc32Tab::getYi_24_32(z[1], z[1 - 1]);
17,276✔
286
                z[1] = Crc32Tab::crc32(z[1 - 1], msb(y[1]));
17,267✔
287
                y[2] = Crc32Tab::getYi_24_32(z[2], z[2 - 1]);
17,275✔
288
                z[2] = Crc32Tab::crc32(z[2 - 1], msb(y[2]));
17,281✔
289
                y[3] = Crc32Tab::getYi_24_32(z[3], z[3 - 1]);
17,286✔
290
                z[3] = Crc32Tab::crc32(z[3 - 1], msb(y[3]));
17,286✔
291
                y[4] = Crc32Tab::getYi_24_32(z[4], z[4 - 1]);
17,281✔
292
                // z[4] = Crc32Tab::crc32(z[4 - 1], msb(y[4])); // this one is already known
293

294
                // recursively complete Y values and derive password
295
                searchRecursive(5);
17,282✔
296
            }
297

298
            prefix.pop_back();
×
299
        }
300
        else // bruteforce the next character and continue recursively
301
        {
302
            prefix.push_back(charset[0]);
18,701✔
303

304
            for (const auto pi : charset)
711,461✔
305
            {
306
                Keys init = initial;
685,438✔
307
                init.update(pi);
685,438✔
308

309
                prefix.back() = pi;
681,356✔
310

311
                searchLongRecursive(init);
683,767✔
312
            }
313

314
            prefix.pop_back();
17,189✔
315
        }
316
    }
317

318
    /// \brief Look for password of length 10 or more
319
    ///
320
    /// Recursively iterate on possible prefixes of length-6 characters.
321
    /// Start parallel workers looking for a password of length 9.
322
    ///
323
    /// \pre prefix.size() + 9 < length
324
    void searchLongParallelRecursive(const Keys& initial, const std::string& start, std::string& restart, int jobs)
5✔
325
    {
326
        const auto charsetSize = static_cast<int>(charset.size());
5✔
327

328
        auto index_start = 0;
5✔
329
        if (prefix.size() < start.size())
5✔
330
            while (index_start < charsetSize && charset[index_start] < static_cast<unsigned char>(start[prefix.size()]))
×
331
                ++index_start;
×
332

333
        if (prefix.size() + 1 + 9 == length) // bruteforce one character in parallel
5✔
334
        {
335
            prefix.push_back(charset[0]);
2✔
336

337
            progress.done += index_start * charsetSize;
2✔
338

339
            const auto threadCount        = std::clamp(jobs, 1, charsetSize);
2✔
340
            auto       threads            = std::vector<std::thread>{};
2✔
341
            auto       nextCandidateIndex = std::atomic<int>{index_start};
2✔
342
            for (auto i = 0; i < threadCount; ++i)
10✔
343
                threads.emplace_back(
8✔
344
                    [&nextCandidateIndex, charsetSize, clone = *this, initial]() mutable
8✔
345
                    {
346
                        for (auto i = nextCandidateIndex++; i < charsetSize; i = nextCandidateIndex++)
83✔
347
                        {
348
                            const auto pm4 = clone.charset[i];
79✔
349

350
                            auto init = initial;
79✔
351
                            init.update(pm4);
79✔
352

353
                            clone.prefix.back() = pm4;
79✔
354

355
                            clone.searchLongRecursive(init);
79✔
356

357
                            clone.progress.done += charsetSize;
79✔
358

359
                            if (clone.progress.state != Progress::State::Normal)
79✔
360
                                break;
4✔
361
                        }
362
                    });
8✔
363
            for (auto& thread : threads)
10✔
364
                thread.join();
8✔
365

366
            prefix.pop_back();
2✔
367

368
            if (nextCandidateIndex < charsetSize)
2✔
369
            {
370
                restart = prefix;
1✔
371
                restart.push_back(charset[nextCandidateIndex]);
1✔
372
                restart.append(length - 6 - restart.size(), charset[0]);
1✔
373
            }
374
        }
2✔
375
        else if (prefix.size() + 2 + 9 == length) // bruteforce two characters in parallel
3✔
376
        {
377
            index_start *= charsetSize;
2✔
378
            if (prefix.size() + 1 < start.size())
2✔
379
            {
380
                const auto maxIndex = std::min(charsetSize * charsetSize, index_start + charsetSize);
×
381
                while (index_start < maxIndex &&
×
382
                       charset[index_start % charsetSize] < static_cast<unsigned char>(start[prefix.size() + 1]))
×
383
                    ++index_start;
×
384
            }
385

386
            prefix.push_back(charset[0]);
2✔
387
            prefix.push_back(charset[0]);
2✔
388

389
            const auto reportProgress       = prefix.size() == 2;
2✔
390
            const auto reportProgressCoarse = prefix.size() == 3;
2✔
391

392
            if (reportProgress)
2✔
393
                progress.done += index_start;
1✔
394
            else if (reportProgressCoarse)
1✔
395
                progress.done += index_start / charsetSize;
1✔
396

397
            const auto threadCount        = std::clamp(jobs, 1, charsetSize * charsetSize);
2✔
398
            auto       threads            = std::vector<std::thread>{};
2✔
399
            auto       nextCandidateIndex = std::atomic<int>{index_start};
2✔
400
            for (auto i = 0; i < threadCount; ++i)
10✔
401
                threads.emplace_back(
8✔
402
                    [&nextCandidateIndex, charsetSize, clone = *this, initial, reportProgress,
8✔
403
                     reportProgressCoarse]() mutable
404
                    {
405
                        for (auto i = nextCandidateIndex++; i < charsetSize * charsetSize; i = nextCandidateIndex++)
709✔
406
                        {
407
                            const auto pm4 = clone.charset[i / charsetSize];
705✔
408
                            const auto pm3 = clone.charset[i % charsetSize];
705✔
409

410
                            auto init = initial;
705✔
411
                            init.update(pm4);
705✔
412
                            init.update(pm3);
705✔
413

414
                            clone.prefix[clone.prefix.size() - 2] = pm4;
705✔
415
                            clone.prefix[clone.prefix.size() - 1] = pm3;
705✔
416

417
                            clone.searchLongRecursive(init);
705✔
418

419
                            if (reportProgress || (reportProgressCoarse && i % charsetSize == charsetSize - 1))
705✔
420
                                clone.progress.done++;
677✔
421

422
                            if (clone.progress.state != Progress::State::Normal)
705✔
423
                                break;
4✔
424
                        }
425
                    });
8✔
426
            for (auto& thread : threads)
10✔
427
                thread.join();
8✔
428

429
            prefix.pop_back();
2✔
430
            prefix.pop_back();
2✔
431

432
            if (nextCandidateIndex < charsetSize * charsetSize)
2✔
433
            {
434
                restart = prefix;
1✔
435
                restart.push_back(charset[nextCandidateIndex / charsetSize]);
1✔
436
                restart.push_back(charset[nextCandidateIndex % charsetSize]);
1✔
437
                restart.append(length - 6 - restart.size(), charset[0]);
1✔
438
            }
439
        }
2✔
440
        else // try password prefixes recursively
441
        {
442
            prefix.push_back(charset[0]);
1✔
443

444
            const auto reportProgress = prefix.size() == 2;
1✔
445

446
            if (prefix.size() == 1)
1✔
447
                progress.done += index_start * charsetSize;
1✔
448
            else if (reportProgress)
×
449
                progress.done += index_start;
×
450

451
            for (auto i = index_start; i < charsetSize; i++)
1✔
452
            {
453
                const auto pi = charset[i];
1✔
454

455
                auto init = initial;
1✔
456
                init.update(pi);
1✔
457

458
                prefix.back() = pi;
1✔
459

460
                if (progress.state != Progress::State::Normal)
1✔
461
                {
462
                    restart = prefix;
×
463
                    restart.resize(length - 6, charset[0]);
×
464
                    break;
1✔
465
                }
466

467
                searchLongParallelRecursive(init, i == index_start ? start : "", restart, jobs);
1✔
468

469
                // Because the recursive call may explore only a fraction of its
470
                // search space, check that it was run in full before counting progress.
471
                if (!restart.empty())
1✔
472
                    break;
1✔
473

474
                if (reportProgress)
×
475
                    progress.done++;
×
476
            }
477

478
            prefix.pop_back();
1✔
479
        }
480
    }
5✔
481

482
    /// Length of the password to recover
483
    std::size_t length;
484

485
    /// The first characters of the password candidate, up to length-6 characters long
486
    std::string prefix;
487

488
    /// Set of characters to generate password candidates
489
    const std::vector<std::uint8_t>& charset;
490

491
    std::vector<std::string>& solutions; // shared output vector of valid passwords
492
    std::mutex&               solutionsMutex;
493
    const bool                exhaustive;
494
    Progress&                 progress;
495
};
496

497
auto recoverPassword(const Keys& keys, const std::vector<std::uint8_t>& charset, std::size_t minLength,
9✔
498
                     std::size_t maxLength, std::string& start, int jobs, bool exhaustive, Progress& progress)
499
    -> std::vector<std::string>
500
{
501
    auto solutions      = std::vector<std::string>{};
9✔
502
    auto solutionsMutex = std::mutex{};
9✔
503
    auto worker         = BruteforceRecovery{keys, charset, solutions, solutionsMutex, exhaustive, progress};
9✔
504

505
    auto       restart     = std::string{};
9✔
506
    const auto startLength = std::max(minLength, start.empty() ? 0 : start.size() + 6);
9✔
507
    for (auto length = startLength; length <= maxLength; length++)
30✔
508
    {
509
        if (progress.state != Progress::State::Normal)
21✔
510
            break;
×
511

512
        if (length <= 6)
21✔
513
        {
514
            progress.log([](std::ostream& os) { os << "length 0-6..." << std::endl; });
18✔
515

516
            // look for a password of length between 0 and 6
517
            for (auto l = 0; l <= 6; l++)
72✔
518
                worker.search(l);
63✔
519

520
            length = 6; // searching up to length 6 is done
9✔
521
        }
522
        else
523
        {
524
            progress.log([length](std::ostream& os) { os << "length " << length << "..." << std::endl; });
24✔
525
            worker.search(length, length == startLength ? start : "", restart, jobs);
36✔
526
        }
527
    }
528

529
    start = restart;
9✔
530

531
    return solutions;
18✔
532
}
9✔
533

534
class MaskRecovery : public SixCharactersRecovery<MaskRecovery>
535
{
536
public:
537
    MaskRecovery(const Keys& keys, const std::vector<std::vector<std::uint8_t>>& mask,
10✔
538
                 std::vector<std::string>& solutions, std::mutex& solutionsMutex, bool exhaustive, Progress& progress)
539
    : target{keys}
20✔
540
    , mask{mask}
10✔
541
    , solutions{solutions}
10✔
542
    , solutionsMutex{solutionsMutex}
10✔
543
    , exhaustive{exhaustive}
10✔
544
    , progress{progress}
10✔
545
    {
546
    }
10✔
547

548
    void search(const std::string& start, std::string& restart, int jobs)
10✔
549
    {
550
        decisions.clear();
10✔
551

552
        if (mask.size() < 6)
10✔
553
        {
554
            // update target state forward so that there are exactly 6 updates between it and the initial state
555
            auto pastTarget = target;
2✔
556
            for (auto i = mask.size(); i < 6; ++i)
9✔
557
                pastTarget.update('\0');
7✔
558
            setTarget(pastTarget, {'\0'});
4✔
559
            SixCharactersRecovery::search(Keys{});
2✔
560
            return;
2✔
561
        }
562
        else if (mask.size() == 6)
8✔
563
        {
564
            setTarget(target, mask[5]);
1✔
565
            SixCharactersRecovery::search(Keys{});
1✔
566
            return;
1✔
567
        }
568
        else // 7 or more characters
569
        {
570
            if (suffixSize == 0)
7✔
571
                setTarget(target, mask[factorIndex + 5]);
2✔
572

573
            if (parallelDepth == -1)
7✔
574
                searchLongRecursive(Keys{}, target);
5✔
575
            else
576
            {
577
                if (progressDepth)
2✔
578
                {
579
                    auto product = 1;
1✔
580
                    for (auto i = 0u; i < progressDepth; ++i)
3✔
581
                        product *= getCharsetAtDepth(i).size();
2✔
582

583
                    progress.done  = 0;
1✔
584
                    progress.total = product;
1✔
585
                }
586
                searchLongParallelRecursive(Keys{}, target, start, restart, jobs);
2✔
587
            }
588
        }
589
    }
590

591
    void onSolutionFound() const
10✔
592
    {
593
        auto password = std::string{};
10✔
594
        password.append(decisions.begin() + suffixSize, decisions.end());
10✔
595
        password.append(p.begin(), p.end());
10✔
596
        password.append(decisions.rbegin() + factorIndex, decisions.rend());
10✔
597
        password.resize(mask.size());
10✔
598

599
        const auto isInSearchSpace =
600
            std::all_of(password.begin(), password.end(),
10✔
601
                        [this, i = 0](char c) mutable
161✔
602
                        {
603
                            const auto& charset = mask[i++];
161✔
604
                            return std::binary_search(charset.begin(), charset.end(), static_cast<std::uint8_t>(c));
161✔
605
                        });
606

607
        if (!isInSearchSpace)
10✔
608
        {
NEW
609
            progress.log(
×
NEW
610
                [&password](std::ostream& os)
×
611
                {
NEW
612
                    const auto flagsBefore = os.setf(std::ios::hex, std::ios::basefield);
×
NEW
613
                    const auto fillBefore  = os.fill('0');
×
614

NEW
615
                    os << "Password: " << password << " (as bytes:";
×
NEW
616
                    for (const auto c : password)
×
NEW
617
                        os << ' ' << std::setw(2) << int{static_cast<std::uint8_t>(c)};
×
NEW
618
                    os << ')' << std::endl;
×
619

NEW
620
                    os.fill(fillBefore);
×
NEW
621
                    os.flags(flagsBefore);
×
622

NEW
623
                    os << "Some characters do not match the given mask. Continuing." << std::endl;
×
NEW
624
                });
×
625

NEW
626
            return;
×
627
        }
628

629
        {
630
            const auto lock = std::scoped_lock{solutionsMutex};
10✔
631
            solutions.push_back(password);
10✔
632
        }
10✔
633

634
        progress.log([&password](std::ostream& os) { os << "Password: " << password << std::endl; });
20✔
635

636
        if (!exhaustive)
10✔
637
            progress.state = Progress::State::EarlyExit;
10✔
638
    }
10✔
639

640
private:
641
    /// \brief Look for password of length 7 or more
642
    ///
643
    /// Recursively iterate on possible reversed suffixes and prefixes, for a total of mask.size()-6 characters.
644
    /// For each case, try to recover the last 6 characters with SixCharactersRecovery::search.
645
    ///
646
    /// \pre decisions.size() + 6 < mask.size()
647
    void searchLongRecursive(const Keys& afterPrefix, const Keys& beforeSuffix)
591,862✔
648
    {
649
        const auto depth = decisions.size();
591,862✔
650

651
        if (depth + 7 == mask.size()) // there is only one more character to bruteforce
593,852✔
652
        {
653
            if (depth < suffixSize) // bruteforce the last remaining suffix character
353,540✔
654
            {
655
                decisions.emplace_back();
2✔
656

657
                for (const auto pi : getCharsetAtDepth(depth))
4✔
658
                {
659
                    auto beforeSuffix2 = beforeSuffix;
2✔
660
                    beforeSuffix2.updateBackwardPlaintext(pi);
2✔
661
                    setTarget(beforeSuffix2, mask[factorIndex + 5]);
2✔
662

663
                    decisions.back() = pi;
2✔
664

665
                    SixCharactersRecovery::search(afterPrefix);
2✔
666
                }
667

668
                decisions.pop_back();
2✔
669
            }
670
            else // bruteforce the last remaining prefix character
671
            {
672
                // check compatible Z{-1}[24, 32)
673
                if (!zm1_24_32[afterPrefix.getZ() >> 24])
353,538✔
674
                    return;
340,283✔
675

676
                decisions.emplace_back();
13,437✔
677

678
                // precompute as much as we can about the next cipher state without knowing the password byte
679
                const auto x0_partial = Crc32Tab::crc32(afterPrefix.getX(), 0);
13,428✔
680
                const auto y0_partial = afterPrefix.getY() * MultTab::mult + 1;
13,422✔
681
                const auto z0_partial = Crc32Tab::crc32(afterPrefix.getZ(), 0);
13,421✔
682

683
                for (const auto pi : getCharsetAtDepth(depth))
347,258✔
684
                {
685
                    // finish to update the cipher state
686
                    const auto x0 = x0_partial ^ Crc32Tab::crc32(0, pi);
337,448✔
687
                    const auto y0 = y0_partial + MultTab::mult * lsb(x0);
330,519✔
688
                    const auto z0 = z0_partial ^ Crc32Tab::crc32(0, msb(y0));
331,841✔
689

690
                    // SixCharactersRecovery::search is inlined below for performance
691

692
                    // check compatible Z0[16,32)
693
                    if (!z0_16_32[z0 >> 16])
339,737✔
694
                        continue;
332,368✔
695

696
                    decisions.back() = pi;
1,455✔
697

698
                    // initialize starting X, Y and Z values
699
                    x[0] = candidateX0 = x0;
1,455✔
700
                    y[0]               = y0;
1,455✔
701
                    z[0]               = z0;
1,455✔
702

703
                    // complete Z values and derive Y[24,32) values
704
                    y[1] = Crc32Tab::getYi_24_32(z[1], z[1 - 1]);
1,455✔
705
                    z[1] = Crc32Tab::crc32(z[1 - 1], msb(y[1]));
1,455✔
706
                    y[2] = Crc32Tab::getYi_24_32(z[2], z[2 - 1]);
1,456✔
707
                    z[2] = Crc32Tab::crc32(z[2 - 1], msb(y[2]));
1,455✔
708
                    y[3] = Crc32Tab::getYi_24_32(z[3], z[3 - 1]);
1,456✔
709
                    z[3] = Crc32Tab::crc32(z[3 - 1], msb(y[3]));
1,456✔
710
                    y[4] = Crc32Tab::getYi_24_32(z[4], z[4 - 1]);
1,456✔
711
                    // z[4] = Crc32Tab::crc32(z[4 - 1], msb(y[4])); // this one is already known
712

713
                    // recursively complete Y values and derive password
714
                    searchRecursive(5);
1,455✔
715
                }
716

717
                decisions.pop_back();
7,119✔
718
            }
719
        }
720
        else // bruteforce the next character and continue recursively
721
        {
722
            decisions.emplace_back();
300,692✔
723

724
            if (depth < suffixSize) // bruteforce the next suffix character
305,797✔
725
            {
726
                for (const auto pi : getCharsetAtDepth(depth))
90,795✔
727
                {
728
                    auto beforeSuffix2 = beforeSuffix;
52,896✔
729
                    beforeSuffix2.updateBackwardPlaintext(pi);
52,896✔
730
                    if (depth + 1 == suffixSize)
52,917✔
731
                        setTarget(beforeSuffix2, mask[factorIndex + 5]);
15,167✔
732

733
                    decisions.back() = pi;
52,931✔
734

735
                    searchLongRecursive(afterPrefix, beforeSuffix2);
52,878✔
736
                }
737
            }
738
            else // bruteforce the next prefix character
739
            {
740
                for (const auto pi : getCharsetAtDepth(depth))
849,605✔
741
                {
742
                    auto afterPrefix2 = afterPrefix;
588,953✔
743
                    afterPrefix2.update(pi);
588,953✔
744

745
                    decisions.back() = pi;
578,618✔
746

747
                    searchLongRecursive(afterPrefix2, beforeSuffix);
571,123✔
748
                }
749
            }
750

751
            decisions.pop_back();
311,612✔
752
        }
753
    }
754

755
    /// Look for password recursively up to depth parallelDepth.
756
    /// Then parallelize the next two decisions and continue exploration with searchLongRecursive.
757
    void searchLongParallelRecursive(const Keys& afterPrefix, const Keys& beforeSuffix, const std::string& start,
3✔
758
                                     std::string& restart, int jobs)
759
    {
760
        const auto  depth   = decisions.size();
3✔
761
        const auto& charset = getCharsetAtDepth(depth);
3✔
762

763
        auto index_start = 0;
3✔
764
        if (decisions.size() < start.size())
3✔
NEW
765
            while (index_start < static_cast<int>(charset.size()) &&
×
NEW
766
                   charset[index_start] < static_cast<unsigned char>(start[depth]))
×
NEW
767
                ++index_start;
×
768

769
        if (static_cast<int>(depth) == parallelDepth) // parallelize the next two decisions
3✔
770
        {
771
            const auto& nextCharset       = getCharsetAtDepth(depth + 1);
2✔
772
            const auto  parallelSpaceSize = static_cast<int>(charset.size() * nextCharset.size());
2✔
773

774
            index_start *= nextCharset.size();
2✔
775
            if (decisions.size() + 1 < start.size())
2✔
776
            {
NEW
777
                const auto maxIndex = std::min(parallelSpaceSize, index_start + static_cast<int>(nextCharset.size()));
×
NEW
778
                while (index_start < maxIndex && nextCharset[index_start % nextCharset.size()] <
×
NEW
779
                                                     static_cast<unsigned char>(start[decisions.size() + 1]))
×
NEW
780
                    ++index_start;
×
781
            }
782

783
            decisions.resize(depth + 2);
2✔
784

785
            const auto reportProgress       = decisions.size() == progressDepth;
2✔
786
            const auto reportProgressCoarse = decisions.size() == progressDepth + 1;
2✔
787

788
            if (reportProgress)
2✔
789
                progress.done += index_start;
1✔
790
            else if (reportProgressCoarse)
1✔
NEW
791
                progress.done += index_start / nextCharset.size();
×
792

793
            const auto threadCount        = std::clamp(jobs, 1, parallelSpaceSize);
2✔
794
            auto       threads            = std::vector<std::thread>{};
2✔
795
            auto       nextCandidateIndex = std::atomic<int>{index_start};
2✔
796
            for (auto i = 0; i < threadCount; ++i)
10✔
797
                threads.emplace_back(
8✔
798
                    [beforeSuffix, afterPrefix, &nextCandidateIndex, &charset, &nextCharset, depth, parallelSpaceSize,
8✔
799
                     clone = *this, reportProgress, reportProgressCoarse]() mutable
800
                    {
801
                        for (auto i = nextCandidateIndex++; i < parallelSpaceSize; i = nextCandidateIndex++)
60✔
802
                        {
803
                            const auto firstChoice  = charset[i / nextCharset.size()];
60✔
804
                            const auto secondChoice = nextCharset[i % nextCharset.size()];
60✔
805

806
                            auto afterPrefix2  = afterPrefix;
60✔
807
                            auto beforeSuffix2 = beforeSuffix;
60✔
808

809
                            if (depth < clone.suffixSize)
60✔
810
                            {
811
                                beforeSuffix2.updateBackwardPlaintext(firstChoice);
60✔
812
                                if (depth + 1 == clone.suffixSize)
60✔
NEW
813
                                    clone.setTarget(beforeSuffix2, clone.mask[clone.factorIndex + 5]);
×
814
                            }
815
                            else
NEW
816
                                afterPrefix2.update(firstChoice);
×
817

818
                            if (depth + 1 < clone.suffixSize)
60✔
819
                            {
820
                                beforeSuffix2.updateBackwardPlaintext(secondChoice);
60✔
821
                                if (depth + 2 == clone.suffixSize)
60✔
NEW
822
                                    clone.setTarget(beforeSuffix2, clone.mask[clone.factorIndex + 5]);
×
823
                            }
824
                            else
NEW
825
                                afterPrefix2.update(secondChoice);
×
826

827
                            clone.decisions[depth]     = firstChoice;
60✔
828
                            clone.decisions[depth + 1] = secondChoice;
60✔
829

830
                            clone.searchLongRecursive(afterPrefix2, beforeSuffix2);
60✔
831

832
                            if (reportProgress ||
76✔
833
                                (reportProgressCoarse && i % nextCharset.size() == nextCharset.size() - 1))
16✔
834
                                clone.progress.done++;
44✔
835

836
                            if (clone.progress.state != Progress::State::Normal)
60✔
837
                                break;
8✔
838
                        }
839
                    });
8✔
840
            for (auto& thread : threads)
10✔
841
                thread.join();
8✔
842

843
            decisions.resize(depth);
2✔
844

845
            if (nextCandidateIndex < parallelSpaceSize)
2✔
846
            {
847
                restart = std::string{decisions.begin(), decisions.end()};
4✔
848
                restart.push_back(charset[nextCandidateIndex / nextCharset.size()]);
2✔
849
                restart.push_back(nextCharset[nextCandidateIndex % nextCharset.size()]);
2✔
850
                while (restart.size() < mask.size() - 6)
24✔
851
                    restart.push_back(getCharsetAtDepth(restart.size())[0]);
22✔
852
            }
853
        }
2✔
854
        else // take next decisions recursively
855
        {
856
            decisions.emplace_back();
1✔
857

858
            const auto reportProgress = depth + 1 == progressDepth;
1✔
859

860
            if (depth + 1 <= progressDepth)
1✔
861
            {
NEW
862
                auto subSearchSize = 1;
×
NEW
863
                for (auto i = depth + 1; i < progressDepth; ++i)
×
NEW
864
                    subSearchSize *= getCharsetAtDepth(i).size();
×
NEW
865
                progress.done += index_start * subSearchSize;
×
866
            }
867

868
            for (auto i = index_start; i < static_cast<int>(charset.size()); i++)
1✔
869
            {
870
                const auto pi = charset[i];
1✔
871

872
                auto afterPrefix2  = afterPrefix;
1✔
873
                auto beforeSuffix2 = beforeSuffix;
1✔
874
                if (depth < suffixSize)
1✔
875
                {
876
                    beforeSuffix2.updateBackwardPlaintext(pi);
1✔
877
                    if (depth + 1 == suffixSize)
1✔
NEW
878
                        setTarget(beforeSuffix2, mask[factorIndex + 5]);
×
879
                }
880
                else
NEW
881
                    afterPrefix2.update(pi);
×
882

883
                decisions.back() = pi;
1✔
884

885
                if (progress.state != Progress::State::Normal)
1✔
886
                {
NEW
887
                    restart = std::string{decisions.begin(), decisions.end()};
×
NEW
888
                    while (restart.size() < mask.size() - 6)
×
NEW
889
                        restart.push_back(getCharsetAtDepth(restart.size())[0]);
×
890
                    break;
1✔
891
                }
892

893
                searchLongParallelRecursive(afterPrefix2, beforeSuffix2, i == index_start ? start : "", restart, jobs);
1✔
894

895
                // Because the recursive call may explore only a fraction of its
896
                // search space, check that it was run in full before counting progress.
897
                if (!restart.empty())
1✔
898
                    break;
1✔
899

NEW
900
                if (reportProgress)
×
NEW
901
                    progress.done++;
×
902
            }
903

904
            decisions.pop_back();
1✔
905
        }
906
    }
3✔
907

908
    /// Internal representation of the password to recover
909
    const Keys target;
910

911
    /// Sequence of charsets to generate password candidates
912
    const std::vector<std::vector<std::uint8_t>>& mask;
913

914
    /// \brief factor start index
915
    ///
916
    /// The mask is split in three parts: prefix, 6 characters factor, suffix.
917
    /// We want to choose the factor start index f that minimizes the password search computational cost.
918
    /// The search cost is estimated with the following linear combination:
919
    ///     cost(f) = 0.70 * a(f) + 0.24 * b(f) + 0.06 * c(f)
920
    /// where:
921
    ///     a(f) = s(f) * mask[f + 5].size()                                 (setTarget iterations)
922
    ///     b(f) = s(f) * p(f) * (1.0 - zm1Ratio(f)) / mask[f - 1].size()    (early exits on zm1_24_32 mismatch)
923
    ///     c(f) = s(f) * p(f) * zm1Ratio(f)                                 (SixCharactersRecovery::search calls)
924
    ///     s(f) = product of mask[i].size() for i in [f + 6 .. mask.size())
925
    ///     p(f) = product of mask[i].size() for i in [0 .. f)
926
    ///     zm1Ratio(f) = mask[f + 5].size() / 256.0
927
    /// Weights 0.70, 0.24, 0.06 have been determined by experiment (time measurement and linear regression).
928
    const std::size_t factorIndex = [this]
10✔
929
    {
930
        if (mask.size() < 7)
10✔
931
            return std::size_t{0};
3✔
932

933
        const auto cost = [this](std::size_t f) -> double
115✔
934
        {
935
            auto p = 1.0;
115✔
936
            for (auto i = 0u; i < f; ++i)
1,194✔
937
                p *= mask[i].size();
1,079✔
938

939
            auto s = 1.0;
115✔
940
            for (auto i = f + 6; i < mask.size(); ++i)
1,194✔
941
                s *= mask[i].size();
1,079✔
942

943
            const auto m5       = static_cast<double>(mask[f + 5].size());
115✔
944
            const auto mm1      = f ? static_cast<double>(mask[f - 1].size()) : 1.0;
115✔
945
            const auto zm1Ratio = f ? m5 / 256.0 : 1.0;
115✔
946

947
            const auto a = s * m5;
115✔
948
            const auto b = s * p * (1.0 - zm1Ratio) / mm1;
115✔
949
            const auto c = s * p * zm1Ratio;
115✔
950

951
            return 0.70 * a + 0.24 * b + 0.06 * c;
115✔
952
        };
7✔
953

954
        auto best = std::pair{std::size_t{0}, cost(0)};
7✔
955
        for (auto f = std::size_t{1}; f + 6 <= mask.size(); ++f)
115✔
956
            if (const auto c = cost(f); c <= best.second)
108✔
957
                best = {f, c};
24✔
958

959
        return best.first;
7✔
960
    }();
961

962
    const std::size_t suffixSize = mask.size() < 7 ? 0 : mask.size() - factorIndex - 6;
963

964
    /// \brief Get the charset for the given depth of recursive exploration
965
    ///
966
    /// Mask charsets are explored recursively starting from the suffix charsets in reverse order,
967
    /// then followed by the prefix charsets.
968
    const std::vector<std::uint8_t>& getCharsetAtDepth(std::size_t i)
317,162✔
969
    {
970
        if (i < suffixSize)
317,162✔
971
            return mask[mask.size() - 1 - i];
38,023✔
972
        else
973
            return mask[i - suffixSize];
279,139✔
974
    }
975

976
    /// \brief Depth of the recursive exploration at which parallel workers are started
977
    ///
978
    /// -1 means the recursive exploration is not parallelized.
979
    const int parallelDepth = [this]
10✔
980
    {
981
        if (mask.size() < 7)
10✔
982
            return -1;
3✔
983

984
        // Find deepest recursion level with remaining search space of given size or more
985
        const auto getDepthForSize = [this](std::uint64_t size) -> std::size_t
10✔
986
        {
987
            auto product = std::uint64_t{1};
10✔
988
            for (auto i = mask.size() - 6; 0 < i; --i)
134✔
989
            {
990
                product *= getCharsetAtDepth(i - 1).size();
127✔
991
                if (size <= product)
127✔
992
                    return i - 1;
3✔
993
            }
994
            return 0;
7✔
995
        };
7✔
996

997
        const auto atomicWorkDepth = getDepthForSize(1ul << 16);
7✔
998

999
        // not deep enough to warrant parallelization
1000
        if (atomicWorkDepth < 2)
7✔
1001
            return -1;
4✔
1002

1003
        // Parallelizing earlier than this depth might make the program slow to stop
1004
        // upon SIGINT or upon finding a solution.
1005
        const auto shallowestParallelDepth = getDepthForSize(1ul << 32);
3✔
1006

1007
        // Find two consecutive characters to be guessed in parallel, maximizing the work items count.
1008
        auto best = std::pair{-1, std::size_t{1}};
3✔
1009
        for (auto i = shallowestParallelDepth; i + 1 < atomicWorkDepth; ++i)
28✔
1010
            if (const auto product = getCharsetAtDepth(i).size() * getCharsetAtDepth(i + 1).size();
25✔
1011
                best.second < product)
25✔
1012
                best = {i, product};
2✔
1013

1014
        return best.first;
3✔
1015
    }();
1016

1017
    /// \brief Depth of the recursive exploration at which progress is counted
1018
    ///
1019
    /// 0 means progress is not reported.
1020
    const std::size_t progressDepth = [this]
10✔
1021
    {
1022
        // Progress is not reported when the recursive exploration is not paralellized, because it is fast.
1023
        if (parallelDepth < 0)
10✔
1024
            return 0;
8✔
1025

1026
        // Find first recursion level with 100 or more iterations
1027
        auto product = 1;
2✔
1028
        for (auto i = 0; i < parallelDepth + 2; ++i)
6✔
1029
        {
1030
            product *= getCharsetAtDepth(i).size();
5✔
1031
            if (100 <= product)
5✔
1032
                return i + 1;
1✔
1033
        }
1034

1035
        return 0;
1✔
1036
    }();
1037

1038
    std::vector<std::uint8_t> decisions{}; // sequence of choices to build reversed(suffix) + prefix
1039

1040
    std::vector<std::string>& solutions; // shared output vector of valid passwords
1041
    std::mutex&               solutionsMutex;
1042
    const bool                exhaustive;
1043
    Progress&                 progress;
1044
};
1045

1046
auto recoverPassword(const Keys& keys, const std::vector<std::vector<std::uint8_t>>& mask, std::string& start, int jobs,
10✔
1047
                     bool exhaustive, Progress& progress) -> std::vector<std::string>
1048
{
1049
    auto solutions      = std::vector<std::string>{};
10✔
1050
    auto solutionsMutex = std::mutex{};
10✔
1051
    auto worker         = MaskRecovery{keys, mask, solutions, solutionsMutex, exhaustive, progress};
10✔
1052

1053
    auto restart = std::string{};
10✔
1054
    worker.search(start, restart, jobs);
10✔
1055

1056
    start = restart;
10✔
1057

1058
    return solutions;
20✔
1059
}
10✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc