• 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

61.45
/src/Arguments.cpp
1
#include "Arguments.hpp"
2

3
#include "Zip.hpp"
4
#include "file.hpp"
5

6
#include <algorithm>
7
#include <bitset>
8
#include <thread>
9
#include <type_traits>
10
#include <variant>
11

12
namespace
13
{
14

15
auto charRange(std::uint8_t first, std::uint8_t last) -> std::bitset<256>
180✔
16
{
17
    auto bitset = std::bitset<256>{};
540✔
18

19
    do
20
    {
21
        bitset.set(first);
12,420✔
22
    } while (first++ != last);
12,420✔
23

24
    return bitset;
180✔
25
}
26

27
auto bitsetToVector(const std::bitset<256>& charset) -> std::vector<std::uint8_t>
67✔
28
{
29
    auto vector = std::vector<std::uint8_t>{};
67✔
30
    for (auto c = 0; c < 256; c++)
17,219✔
31
        if (charset[c])
17,152✔
32
            vector.push_back(c);
2,823✔
33

34
    return vector;
67✔
NEW
35
}
×
36

37
template <typename F>
38
auto translateIntParseError(F&& f, const std::string& value)
26✔
39
{
40
    try
41
    {
42
        return f(value);
26✔
43
    }
44
    catch (const std::invalid_argument&)
×
45
    {
46
        throw Arguments::Error{"expected an integer, got \"" + value + "\""};
×
47
    }
48
    catch (const std::out_of_range&)
×
49
    {
50
        throw Arguments::Error{"integer value " + value + " is out of range"};
×
51
    }
52
}
53

54
auto parseInt(const std::string& value) -> int
2✔
55
{
56
    return translateIntParseError([](const std::string& value) { return std::stoi(value, nullptr, 0); }, value);
4✔
57
}
58

59
auto parseSize(const std::string& value) -> std::size_t
11✔
60
{
61
    return translateIntParseError([](const std::string& value) { return std::stoull(value, nullptr, 0); }, value);
22✔
62
}
63

64
auto parseInterval(const std::string& value) -> std::variant<Arguments::LengthInterval, std::size_t>
11✔
65
{
66
    const auto separator = std::string{".."};
11✔
67

68
    if (const auto minEnd = value.find(separator); minEnd != std::string::npos)
11✔
69
    {
70
        auto interval = Arguments::LengthInterval{};
×
71

72
        if (0 < minEnd)
×
73
            interval.minLength = parseSize(value.substr(0, minEnd));
×
74

75
        if (const auto maxBegin = minEnd + separator.size(); maxBegin < value.size())
×
76
            interval.maxLength = parseSize(value.substr(maxBegin));
×
77

78
        return interval;
×
79
    }
80
    else
81
        return parseSize(value);
11✔
82
}
11✔
83

84
} // namespace
85

86
Arguments::Error::Error(const std::string& description)
2✔
87
: BaseError{"Arguments error", description}
4✔
88
{
89
}
2✔
90

91
Arguments::Arguments(int argc, const char* argv[])
30✔
92
: jobs{[]() -> int
60✔
93
       {
94
           const auto concurrency = std::thread::hardware_concurrency();
30✔
95
           return concurrency ? concurrency : 2;
30✔
96
       }()}
30✔
97
, m_current{argv + 1}
30✔
98
, m_end{argv + argc}
30✔
99
, m_charsets{
30✔
NEW
100
      []
×
101
      {
102
          const auto lowercase    = charRange('a', 'z');
30✔
103
          const auto uppercase    = charRange('A', 'Z');
30✔
104
          const auto digits       = charRange('0', '9');
30✔
105
          const auto alphanum     = lowercase | uppercase | digits;
30✔
106
          const auto printable    = charRange(' ', '~');
30✔
107
          const auto punctuation  = printable & ~alphanum;
30✔
108
          const auto bytes        = charRange('\x00', '\xff');
30✔
109
          const auto questionMark = charRange('?', '?');
30✔
110

111
          return std::unordered_map<char, std::bitset<256>>{
NEW
112
              {'l', lowercase}, {'u', uppercase},   {'d', digits}, {'a', alphanum},
×
NEW
113
              {'p', printable}, {'s', punctuation}, {'b', bytes},  {'?', questionMark},
×
114
          };
90✔
115
      }(),
116
  }
60✔
117
{
118
    // parse arguments
119
    while (!finished())
107✔
120
        parseArgument();
77✔
121

122
    if (help || version || infoArchive)
30✔
123
        return; // no further checks are needed for those options
3✔
124

125
    // deferred computations
126
    if (m_rawBruteforce)
27✔
127
        bruteforce = bitsetToVector(resolveCharset(*m_rawBruteforce));
11✔
128
    if (m_rawMask)
25✔
129
    {
130
        mask.emplace();
10✔
131
        for (auto it = m_rawMask->begin(); it != m_rawMask->end(); ++it)
171✔
132
        {
133
            if (*it == '?') // escape character to reference other charsets
161✔
134
            {
135
                if (++it == m_rawMask->end())
58✔
136
                {
NEW
137
                    mask->push_back({'?'});
×
NEW
138
                    break;
×
139
                }
140

141
                mask->push_back(bitsetToVector(resolveCharset(std::string{"?"} + *it)));
174✔
142
            }
143
            else
144
                mask->push_back({static_cast<std::uint8_t>(*it)});
309✔
145
        }
146
    }
147

148
    // check constraints on arguments
149
    if (keys)
25✔
150
    {
151
        if (!decipheredFile && !decryptedArchive && !changePassword && !changeKeys && !bruteforce && !mask)
19✔
NEW
152
            throw Error{"-d, -D, -U, --change-keys, --bruteforce or --mask parameter is missing (required by -k)"};
×
153
    }
154
    else if (!password)
6✔
155
    {
156
        if (cipherFile && cipherIndex)
1✔
157
            throw Error{"-c and --cipher-index cannot be used at the same time"};
×
158
        if (plainFile && plainIndex)
1✔
159
            throw Error{"-p and --plain-index cannot be used at the same time"};
×
160

161
        if (!cipherFile && !cipherIndex)
1✔
162
            throw Error{"-c or --cipher-index parameter is missing"};
×
163
        if (!plainFile && !plainIndex && extraPlaintext.empty())
1✔
164
            throw Error{"-p, --plain-index or -x parameter is missing"};
×
165

166
        if (plainArchive && !plainFile && !plainIndex)
1✔
167
            throw Error{"-p or --plain-index parameter is missing (required by -P)"};
×
168

169
        if (cipherIndex && !cipherArchive)
1✔
170
            throw Error{"-C parameter is missing (required by --cipher-index)"};
×
171
        if (plainIndex && !plainArchive)
1✔
172
            throw Error{"-P parameter is missing (required by --plain-index)"};
×
173

174
        constexpr auto minimumOffset = -static_cast<int>(Data::encryptionHeaderSize);
1✔
175
        if (offset < minimumOffset)
1✔
176
            throw Error{"plaintext offset " + std::to_string(offset) + " is too small (minimum is " +
×
177
                        std::to_string(minimumOffset) + ")"};
×
178
    }
179

180
    if (decipheredFile && !cipherFile && !cipherIndex)
25✔
181
        throw Error{"-c or --cipher-index parameter is missing (required by -d)"};
×
182
    if (decipheredFile && !cipherArchive && decipheredFile == cipherFile)
25✔
183
        throw Error{"-c and -d parameters must point to different files"};
×
184

185
    if (decryptedArchive && !cipherArchive)
25✔
186
        throw Error{"-C parameter is missing (required by -D)"};
×
187
    if (decryptedArchive && decryptedArchive == cipherArchive)
25✔
188
        throw Error{"-C and -D parameters must point to different files"};
×
189

190
    if (changePassword && !cipherArchive)
25✔
191
        throw Error{"-C parameter is missing (required by -U)"};
×
192
    if (changePassword && changePassword->unlockedArchive == cipherArchive)
25✔
193
        throw Error{"-C and -U parameters must point to different files"};
×
194

195
    if (changeKeys && !cipherArchive)
25✔
196
        throw Error{"-C parameter is missing (required by --change-keys)"};
×
197
    if (changeKeys && changeKeys->unlockedArchive == cipherArchive)
25✔
198
        throw Error{"-C and --change-keys parameters must point to different files"};
×
199

200
    if (length && !bruteforce)
25✔
201
        throw Error{"--bruteforce parameter is missing (required by --length)"};
×
202

203
    if (bruteforce && mask)
25✔
NEW
204
        throw Error{"--bruteforce and --mask cannot be used at the same time"};
×
205
}
36✔
206

207
auto Arguments::loadData() const -> Data
1✔
208
{
209
    // load known plaintext
210
    auto plaintext = std::vector<std::uint8_t>{};
1✔
211
    if (plainArchive)
1✔
212
    {
213
        const auto archive = Zip{*plainArchive};
×
214
        const auto entry   = plainFile ? archive[*plainFile] : archive[*plainIndex];
×
215
        Zip::checkEncryption(entry, Zip::Encryption::None);
×
216
        plaintext = archive.load(entry, plainFilePrefix);
×
217
    }
×
218
    else if (plainFile)
1✔
219
        plaintext = loadFile(*plainFile, plainFilePrefix);
×
220

221
    // load ciphertext needed by the attack
222
    auto needed = Data::encryptionHeaderSize;
1✔
223
    if (!plaintext.empty())
1✔
224
        needed = std::max(needed, Data::encryptionHeaderSize + offset + plaintext.size());
×
225
    if (!extraPlaintext.empty())
1✔
226
        needed = std::max(needed, Data::encryptionHeaderSize + extraPlaintext.rbegin()->first + 1);
1✔
227

228
    auto ciphertext                  = std::vector<std::uint8_t>{};
1✔
229
    auto extraPlaintextWithCheckByte = std::optional<std::map<int, std::uint8_t>>{};
1✔
230
    if (cipherArchive)
1✔
231
    {
232
        const auto archive = Zip{*cipherArchive};
1✔
233
        const auto entry   = cipherFile ? archive[*cipherFile] : archive[*cipherIndex];
1✔
234
        Zip::checkEncryption(entry, Zip::Encryption::Traditional);
1✔
235
        ciphertext = archive.load(entry, needed);
1✔
236

237
        if (!ignoreCheckByte && !extraPlaintext.count(-1))
1✔
238
        {
239
            extraPlaintextWithCheckByte        = extraPlaintext;
1✔
240
            (*extraPlaintextWithCheckByte)[-1] = entry.checkByte;
1✔
241
        }
242
    }
1✔
243
    else
244
        ciphertext = loadFile(*cipherFile, needed);
×
245

246
    return {std::move(ciphertext), std::move(plaintext), offset, extraPlaintextWithCheckByte.value_or(extraPlaintext)};
2✔
247
}
1✔
248

249
auto Arguments::LengthInterval::operator&(const Arguments::LengthInterval& other) const -> Arguments::LengthInterval
11✔
250
{
251
    return {std::max(minLength, other.minLength), std::min(maxLength, other.maxLength)};
11✔
252
}
253

254
auto Arguments::resolveCharset(const std::string& rawCharset) -> std::bitset<256>
83✔
255
{
256
    auto charset = std::bitset<256>{};
249✔
257

258
    for (auto it = rawCharset.begin(); it != rawCharset.end(); ++it)
169✔
259
    {
260
        if (*it == '?') // escape character to reference other charsets
96✔
261
        {
262
            if (++it == rawCharset.end())
85✔
263
            {
264
                charset.set('?');
1✔
265
                break;
1✔
266
            }
267

268
            if (const auto rawCharsetsIt = m_rawCharsets.find(*it); rawCharsetsIt != m_rawCharsets.end())
84✔
269
            {
270
                // insert in m_charsets to mark the identifier is being resolved and detect cycles
271
                if (const auto [_, inserted] = m_charsets.try_emplace(*it); !inserted)
16✔
272
                    throw Error{std::string{"circular reference resolving charset ?"} + *it};
6✔
273

274
                m_charsets[*it] = resolveCharset(rawCharsetsIt->second);
14✔
275
                m_rawCharsets.erase(rawCharsetsIt);
7✔
276
            }
277

278
            if (const auto charsetsIt = m_charsets.find(*it); charsetsIt != m_charsets.end())
75✔
279
                charset |= charsetsIt->second;
75✔
280
            else
NEW
281
                throw Error{std::string{"unknown charset ?"} + *it};
×
282
        }
283
        else
284
            charset.set(*it);
11✔
285
    }
286

287
    return charset;
74✔
288
}
289

290
auto Arguments::finished() const -> bool
327✔
291
{
292
    return m_current == m_end;
327✔
293
}
294

295
void Arguments::parseArgument()
77✔
296
{
297
    switch (readOption("an option"))
154✔
298
    {
299
    case Option::cipherFile:
2✔
300
        cipherFile = readString("ciphertext");
2✔
301
        break;
2✔
302
    case Option::cipherIndex:
×
303
        cipherIndex = readSize("index");
×
304
        break;
×
305
    case Option::cipherArchive:
5✔
306
        cipherArchive = readString("encryptedzip");
5✔
307
        break;
5✔
308
    case Option::plainFile:
×
309
        plainFile = readString("plaintext");
×
310
        break;
×
311
    case Option::plainIndex:
×
312
        plainIndex = readSize("index");
×
313
        break;
×
314
    case Option::plainArchive:
×
315
        plainArchive = readString("plainzip");
×
316
        break;
×
317
    case Option::plainFilePrefix:
×
318
        plainFilePrefix = readSize("size");
×
319
        break;
×
320
    case Option::offset:
×
321
        offset = readInt("offset");
×
322
        break;
×
323
    case Option::extraPlaintext:
1✔
324
    {
325
        auto i = readInt("offset");
2✔
326
        for (const auto b : readHex("data"))
22✔
327
            extraPlaintext[i++] = b;
21✔
328
        break;
1✔
329
    }
330
    case Option::ignoreCheckByte:
×
331
        ignoreCheckByte = true;
×
332
        break;
×
333
    case Option::attackStart:
1✔
334
        attackStart = readInt("checkpoint");
1✔
335
        break;
1✔
336
    case Option::password:
7✔
337
        password = readString("password");
7✔
338
        break;
7✔
339
    case Option::keys:
19✔
340
        keys = {readKey("X"), readKey("Y"), readKey("Z")};
95✔
341
        break;
19✔
342
    case Option::decipheredFile:
1✔
343
        decipheredFile = readString("decipheredfile");
1✔
344
        break;
1✔
345
    case Option::keepHeader:
×
346
        keepHeader = true;
×
347
        break;
×
348
    case Option::decryptedArchive:
1✔
349
        decryptedArchive = readString("decipheredzip");
1✔
350
        break;
1✔
351
    case Option::changePassword:
1✔
352
        changePassword = {readString("unlockedzip"), readString("password")};
1✔
353
        break;
1✔
354
    case Option::changeKeys:
1✔
355
        changeKeys = {readString("unlockedzip"), {readKey("X"), readKey("Y"), readKey("Z")}};
1✔
356
        break;
1✔
357
    case Option::bruteforce:
×
NEW
358
        m_rawBruteforce = readRawCharset("charset for bruteforce password recovery");
×
359
        break;
×
360
    case Option::length:
×
361
        length = length.value_or(LengthInterval{}) &
×
362
                 std::visit(
×
363
                     [](auto arg)
×
364
                     {
365
                         if constexpr (std::is_same_v<decltype(arg), std::size_t>)
366
                             return LengthInterval{arg, arg}; // a single value is interpreted as an exact length
×
367
                         else
368
                             return arg;
×
369
                     },
370
                     parseInterval(readString("length")));
×
371
        break;
×
372
    case Option::recoverPassword:
11✔
373
        length = length.value_or(LengthInterval{}) &
11✔
374
                 std::visit(
11✔
375
                     [](auto arg)
22✔
376
                     {
377
                         if constexpr (std::is_same_v<decltype(arg), std::size_t>)
378
                             return LengthInterval{0, arg}; // a single value is interpreted as an interval 0..max
22✔
379
                         else
380
                             return arg;
×
381
                     },
382
                     parseInterval(readString("length")));
33✔
383
        m_rawBruteforce = readRawCharset("charset for bruteforce password recovery");
11✔
384
        break;
11✔
385
    case Option::mask:
10✔
386
        m_rawMask = readString("mask");
10✔
387
        break;
10✔
388
    case Option::charset:
14✔
389
    {
390
        const auto identifier = readString("identifier");
14✔
391
        if (identifier.size() != 1)
14✔
NEW
392
            throw Error{"charset identifier must be a single character, got \"" + identifier + "\""};
×
393
        if (m_charsets.count(identifier[0]) || m_rawCharsets.count(identifier[0]))
14✔
NEW
394
            throw Error{"charset ?" + identifier + " is already defined, it cannot be redefined"};
×
395
        m_rawCharsets[identifier[0]] = readRawCharset("charset ?" + identifier);
14✔
396
        break;
14✔
397
    }
14✔
UNCOV
398
    case Option::recoveryStart:
×
399
    {
400
        const auto checkpoint = readHex("checkpoint");
×
401
        recoveryStart.assign(checkpoint.begin(), checkpoint.end());
×
402
        break;
×
403
    }
×
404
    case Option::jobs:
×
405
        jobs = readInt("count");
×
406
        break;
×
407
    case Option::exhaustive:
×
408
        exhaustive = true;
×
409
        break;
×
410
    case Option::infoArchive:
1✔
411
        infoArchive = readString("zipfile");
1✔
412
        break;
1✔
413
    case Option::version:
1✔
414
        version = true;
1✔
415
        break;
1✔
416
    case Option::help:
1✔
417
        help = true;
1✔
418
        break;
1✔
419
    }
420
}
89✔
421

422
auto Arguments::readString(const std::string& description) -> std::string
220✔
423
{
424
    if (finished())
220✔
425
        throw Error{"expected " + description + ", got nothing"};
×
426

427
    return *m_current++;
440✔
428
}
429

430
auto Arguments::readOption(const std::string& description) -> Arguments::Option
77✔
431
{
432
    // clang-format off
433
#define PAIR(string, option) {#string, Option::option}
434
#define PAIRS(short, long, option) PAIR(short, option), PAIR(long, option)
435

436
    static const auto stringToOption = std::map<std::string, Option>{
437
        PAIRS(-c, --cipher-file,       cipherFile),
×
438
        PAIR (    --cipher-index,      cipherIndex),
×
439
        PAIRS(-C, --cipher-zip,        cipherArchive),
×
440
        PAIRS(-p, --plain-file,        plainFile),
×
441
        PAIR (    --plain-index,       plainIndex),
×
442
        PAIRS(-P, --plain-zip,         plainArchive),
×
443
        PAIRS(-t, --truncate,          plainFilePrefix),
×
444
        PAIRS(-o, --offset,            offset),
×
445
        PAIRS(-x, --extra,             extraPlaintext),
×
446
        PAIR (    --ignore-check-byte, ignoreCheckByte),
×
447
        PAIR (    --continue-attack,   attackStart),
×
448
        PAIR (    --password,          password),
×
449
        PAIRS(-k, --keys,              keys),
×
450
        PAIRS(-d, --decipher,          decipheredFile),
×
451
        PAIR (    --keep-header,       keepHeader),
×
452
        PAIRS(-D, --decrypt,           decryptedArchive),
×
453
        PAIRS(-U, --change-password,   changePassword),
×
454
        PAIR (    --change-keys,       changeKeys),
×
455
        PAIRS(-b, --bruteforce,        bruteforce),
×
456
        PAIRS(-l, --length,            length),
×
457
        PAIRS(-r, --recover-password,  recoverPassword),
×
NEW
458
        PAIRS(-m, --mask,              mask),
×
NEW
459
        PAIRS(-s, --charset,           charset),
×
460
        PAIR (    --continue-recovery, recoveryStart),
×
461
        PAIRS(-j, --jobs,              jobs),
×
462
        PAIRS(-e, --exhaustive,        exhaustive),
×
463
        PAIRS(-L, --list,              infoArchive),
×
464
        PAIR (    --version,           version),
×
465
        PAIRS(-h, --help,              help),
×
466
    };
1,607✔
467
    // clang-format on
468

469
#undef PAIR
470
#undef PAIRS
471

472
    const auto str = readString(description);
77✔
473
    if (const auto it = stringToOption.find(str); it == stringToOption.end())
77✔
474
        throw Error{"unknown option " + str};
×
475
    else
476
        return it->second;
154✔
477
}
107✔
478

479
auto Arguments::readInt(const std::string& description) -> int
2✔
480
{
481
    return parseInt(readString(description));
2✔
482
}
483

484
auto Arguments::readSize(const std::string& description) -> std::size_t
×
485
{
486
    return parseSize(readString(description));
×
487
}
488

489
auto Arguments::readHex(const std::string& description) -> std::vector<std::uint8_t>
1✔
490
{
491
    const auto str = readString(description);
1✔
492

493
    if (str.size() % 2)
1✔
494
        throw Error{"expected an even-length string, got " + str};
×
495
    if (!std::all_of(str.begin(), str.end(), [](char c) { return std::isxdigit(static_cast<unsigned char>(c)); }))
41✔
496
        throw Error{"expected " + description + " in hexadecimal, got " + str};
×
497

498
    auto data = std::vector<std::uint8_t>{};
1✔
499
    for (auto i = std::size_t{}; i < str.length(); i += 2)
21✔
500
        data.push_back(static_cast<std::uint8_t>(std::stoul(str.substr(i, 2), nullptr, 16)));
20✔
501

502
    return data;
2✔
503
}
1✔
504

505
auto Arguments::readKey(const std::string& description) -> std::uint32_t
60✔
506
{
507
    const auto str = readString(description);
60✔
508

509
    if (str.size() > 8)
60✔
510
        throw Error{"expected a string of length 8 or less, got " + str};
×
511
    if (!std::all_of(str.begin(), str.end(), [](char c) { return std::isxdigit(static_cast<unsigned char>(c)); }))
540✔
512
        throw Error{"expected " + description + " in hexadecimal, got " + str};
×
513

514
    return static_cast<std::uint32_t>(std::stoul(str, nullptr, 16));
120✔
515
}
60✔
516

517
auto Arguments::readRawCharset(const std::string& description) -> std::string
25✔
518
{
519
    auto charset = readString(description);
25✔
520

521
    if (charset.empty())
25✔
NEW
522
        throw Error{description + " is empty"};
×
523

524
    return charset;
25✔
UNCOV
525
}
×
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