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

kimci86 / bkcrack / 17050258494

18 Aug 2025 07:19PM UTC coverage: 77.205% (+2.9%) from 74.341%
17050258494

push

github

kimci86
Release v1.8.0

- Implement mask-based password recovery
- Add code coverage report

1558 of 2018 relevant lines covered (77.21%)

1649191.36 hits per line

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

88.35
/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,356✔
19
    {
20
        // initialize target X, Y and Z values
21
        x[6] = target.getX();
30,356✔
22
        y[6] = target.getY();
30,372✔
23
        z[6] = target.getZ();
30,376✔
24

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

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

32
        // precompute possible Z0[16,32) and Z{-1}[24,32)
33
        z0_16_32.reset();
30,388✔
34
        zm1_24_32.reset();
30,456✔
35
        for (const auto p5 : charset5)
271,224✔
36
        {
37
            x[5] = Crc32Tab::crc32inv(x[6], p5);
240,264✔
38
            y[4] = (y[5] - 1) * MultTab::multInv - lsb(x[5]);
236,274✔
39
            z[3] = Crc32Tab::crc32inv(z[4], msb(y[4]));
239,722✔
40

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

45
            z[2] = Crc32Tab::crc32inv(z[3], msb_y3_min);
237,994✔
46
            z[1] = Crc32Tab::crc32inv(z[2], 0);
238,974✔
47
            z[0] = Crc32Tab::crc32inv(z[1], 0);
237,090✔
48

49
            z0_16_32.set(z[0] >> 16);
238,808✔
50
            zm1_24_32.set(Crc32Tab::crc32inv(z[0], 0) >> 24);
240,082✔
51

52
            if (msb_y3_max != msb_y3_min)
240,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
    }
29,660✔
63

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

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

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

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

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

94
            // get possible LSB(Xi)
95
            for (const auto xi_0_8 : MultTab::getMsbProdFiber2(msb(ffy - (y[i - 2] & mask<24, 32>))))
553,926✔
96
            {
97
                // compute corresponding Y{i-1}
98
                const auto yim1 = fy - xi_0_8;
373,216✔
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,292✔
102
                    msb(yim1) == msb(y[i - 1]))
190,446✔
103
                {
104
                    // add Y{i-1} to the Y-list
105
                    y[i - 1] = yim1;
190,450✔
106

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

110
                    searchRecursive(i - 1);
190,462✔
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,632✔
118
            if (x[1] > 0xff)
47,948✔
119
                return;
47,908✔
120

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

129
            if (x[0] == candidateX0) // the password is successfully recovered
40✔
130
                (static_cast<Derived*>(this))->onSolutionFound();
40✔
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,
10✔
152
                       std::mutex& solutionsMutex, bool exhaustive, Progress& progress)
153
    : charset{charset}
20✔
154
    , solutions{solutions}
10✔
155
    , solutionsMutex{solutionsMutex}
10✔
156
    , exhaustive{exhaustive}
10✔
157
    , progress{progress}
10✔
158
    {
159
        setTarget(keys, charset);
10✔
160
    }
10✔
161

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

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

173
        if (length <= 6)
82✔
174
            searchShort();
70✔
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
    }
82✔
184

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

191
        const auto isInCharset =
192
            std::all_of(password.begin(), password.end(),
10✔
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)
10✔
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};
10✔
220
            solutions.push_back(password);
10✔
221
        }
10✔
222

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

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

229
private:
230
    /// \brief Look for a password of length 6 or less
231
    ///
232
    /// \pre prefix.empty() && length <= 6
233
    void searchShort()
70✔
234
    {
235
        auto initial = Keys{};
70✔
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++)
280✔
238
            initial.updateBackwardPlaintext(charset.front());
210✔
239

240
        SixCharactersRecovery::search(initial);
70✔
241
    }
70✔
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)
668,803✔
250
    {
251
        if (prefix.size() + 7 == length) // there is only one more character to bruteforce
668,803✔
252
        {
253
            // check compatible Z{-1}[24, 32)
254
            if (!zm1_24_32[initial.getZ() >> 24])
651,449✔
255
                return;
570,740✔
256

257
            prefix.push_back(charset[0]);
100,061✔
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,063✔
261
            const auto y0_partial = initial.getY() * MultTab::mult + 1;
99,981✔
262
            const auto z0_partial = Crc32Tab::crc32(initial.getZ(), 0);
100,012✔
263

264
            for (const auto pi : charset)
3,260,419✔
265
            {
266
                // finish to update the cipher state
267
                const auto x0 = x0_partial ^ Crc32Tab::crc32(0, pi);
3,204,607✔
268
                const auto y0 = y0_partial + MultTab::mult * lsb(x0);
3,021,627✔
269
                const auto z0 = z0_partial ^ Crc32Tab::crc32(0, msb(y0));
3,070,799✔
270

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

273
                // check compatible Z0[16,32)
274
                if (!z0_16_32[z0 >> 16])
3,158,036✔
275
                    continue;
3,143,054✔
276

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

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

284
                // complete Z values and derive Y[24,32) values
285
                y[1] = Crc32Tab::getYi_24_32(z[1], z[1 - 1]);
17,289✔
286
                z[1] = Crc32Tab::crc32(z[1 - 1], msb(y[1]));
17,285✔
287
                y[2] = Crc32Tab::getYi_24_32(z[2], z[2 - 1]);
17,286✔
288
                z[2] = Crc32Tab::crc32(z[2 - 1], msb(y[2]));
17,286✔
289
                y[3] = Crc32Tab::getYi_24_32(z[3], z[3 - 1]);
17,290✔
290
                z[3] = Crc32Tab::crc32(z[3 - 1], msb(y[3]));
17,291✔
291
                y[4] = Crc32Tab::getYi_24_32(z[4], z[4 - 1]);
17,289✔
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,294✔
296
            }
297

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

304
            for (const auto pi : charset)
707,784✔
305
            {
306
                Keys init = initial;
683,421✔
307
                init.update(pi);
683,421✔
308

309
                prefix.back() = pi;
674,634✔
310

311
                searchLongRecursive(init);
676,646✔
312
            }
313

314
            prefix.pop_back();
16,532✔
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,
10✔
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>{};
10✔
502
    auto solutionsMutex = std::mutex{};
10✔
503
    auto worker         = BruteforceRecovery{keys, charset, solutions, solutionsMutex, exhaustive, progress};
10✔
504

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

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

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

520
            length = 6; // searching up to length 6 is done
10✔
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;
10✔
530

531
    return solutions;
20✔
532
}
10✔
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 afterTarget = target;
2✔
556
            for (auto i = mask.size(); i < 6; ++i)
9✔
557
                afterTarget.update('\0');
7✔
558
            setTarget(afterTarget, {'\0'});
4✔
559
            SixCharactersRecovery::search(Keys{});
2✔
560
        }
561
        else if (mask.size() == 6)
8✔
562
        {
563
            setTarget(target, mask[5]);
1✔
564
            SixCharactersRecovery::search(Keys{});
1✔
565
        }
566
        else // 7 or more characters
567
        {
568
            if (suffixSize == 0)
7✔
569
                setTarget(target, mask[factorIndex + 5]);
2✔
570

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

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

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

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

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

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

618
                    os.fill(fillBefore);
×
619
                    os.flags(flagsBefore);
×
620

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

624
            return;
×
625
        }
626

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

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

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

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

649
        if (depth + 7 == mask.size()) // there is only one more character to bruteforce
574,922✔
650
        {
651
            if (depth < suffixSize) // bruteforce the last remaining suffix character
344,865✔
652
            {
653
                decisions.emplace_back();
2✔
654

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

661
                    decisions.back() = pi;
2✔
662

663
                    SixCharactersRecovery::search(afterPrefix);
2✔
664
                }
665

666
                decisions.pop_back();
2✔
667
            }
668
            else // bruteforce the last remaining prefix character
669
            {
670
                // check compatible Z{-1}[24, 32)
671
                if (!zm1_24_32[afterPrefix.getZ() >> 24])
344,863✔
672
                    return;
334,088✔
673

674
                decisions.emplace_back();
13,408✔
675

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

681
                for (const auto pi : getCharsetAtDepth(depth))
348,057✔
682
                {
683
                    // finish to update the cipher state
684
                    const auto x0 = x0_partial ^ Crc32Tab::crc32(0, pi);
335,076✔
685
                    const auto y0 = y0_partial + MultTab::mult * lsb(x0);
323,745✔
686
                    const auto z0 = z0_partial ^ Crc32Tab::crc32(0, msb(y0));
325,869✔
687

688
                    // SixCharactersRecovery::search is inlined below for performance
689

690
                    // check compatible Z0[16,32)
691
                    if (!z0_16_32[z0 >> 16])
330,239✔
692
                        continue;
333,211✔
693

694
                    decisions.back() = pi;
1,452✔
695

696
                    // initialize starting X, Y and Z values
697
                    x[0] = candidateX0 = x0;
1,452✔
698
                    y[0]               = y0;
1,452✔
699
                    z[0]               = z0;
1,453✔
700

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

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

715
                decisions.pop_back();
6,808✔
716
            }
717
        }
718
        else // bruteforce the next character and continue recursively
719
        {
720
            decisions.emplace_back();
294,089✔
721

722
            if (depth < suffixSize) // bruteforce the next suffix character
298,027✔
723
            {
724
                for (const auto pi : getCharsetAtDepth(depth))
90,715✔
725
                {
726
                    auto beforeSuffix2 = beforeSuffix;
52,840✔
727
                    beforeSuffix2.updateBackwardPlaintext(pi);
52,840✔
728
                    if (depth + 1 == suffixSize)
52,844✔
729
                        setTarget(beforeSuffix2, mask[factorIndex + 5]);
15,160✔
730

731
                    decisions.back() = pi;
52,867✔
732

733
                    searchLongRecursive(afterPrefix, beforeSuffix2);
52,895✔
734
                }
735
            }
736
            else // bruteforce the next prefix character
737
            {
738
                for (const auto pi : getCharsetAtDepth(depth))
830,161✔
739
                {
740
                    auto afterPrefix2 = afterPrefix;
579,250✔
741
                    afterPrefix2.update(pi);
579,250✔
742

743
                    decisions.back() = pi;
557,010✔
744

745
                    searchLongRecursive(afterPrefix2, beforeSuffix);
554,947✔
746
                }
747
            }
748

749
            decisions.pop_back();
308,196✔
750
        }
751
    }
752

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

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

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

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

781
            decisions.resize(depth + 2);
2✔
782

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

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

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

804
                            auto afterPrefix2  = afterPrefix;
60✔
805
                            auto beforeSuffix2 = beforeSuffix;
60✔
806

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

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

825
                            clone.decisions[depth]     = firstChoice;
60✔
826
                            clone.decisions[depth + 1] = secondChoice;
60✔
827

828
                            clone.searchLongRecursive(afterPrefix2, beforeSuffix2);
60✔
829

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

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

841
            decisions.resize(depth);
2✔
842

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

856
            const auto reportProgress = depth + 1 == progressDepth;
1✔
857

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

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

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

881
                decisions.back() = pi;
1✔
882

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

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

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

898
                if (reportProgress)
×
899
                    progress.done++;
×
900
            }
901

902
            decisions.pop_back();
1✔
903
        }
904
    }
3✔
905

906
    /// Internal representation of the password to recover
907
    const Keys target;
908

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

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

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

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

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

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

949
            return 0.70 * a + 0.24 * b + 0.06 * c;
115✔
950
        };
7✔
951

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

957
        return best.first;
7✔
958
    }();
959

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

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

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

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

995
        const auto atomicWorkDepth = getDepthForSize(1ul << 16);
7✔
996

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

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

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

1012
        return best.first;
3✔
1013
    }();
1014

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

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

1033
        return 0;
1✔
1034
    }();
1035

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

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

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

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

1054
    start = restart;
10✔
1055

1056
    return solutions;
20✔
1057
}
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

© 2025 Coveralls, Inc