• 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

66.98
/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>
186✔
16
{
17
    auto bitset = std::bitset<256>{};
558✔
18

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

24
    return bitset;
186✔
25
}
26

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

34
    return vector;
68✔
35
}
×
36

37
template <typename F>
38
auto translateIntParseError(F&& f, const std::string& value)
28✔
39
{
40
    try
41
    {
42
        return f(value);
28✔
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
12✔
60
{
61
    return translateIntParseError([](const std::string& value) { return std::stoull(value, nullptr, 0); }, value);
24✔
62
}
63

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

68
    if (const auto minEnd = value.find(separator); minEnd != std::string::npos)
12✔
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);
12✔
82
}
12✔
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[])
31✔
92
: jobs{[]() -> int
62✔
93
       {
94
           const auto concurrency = std::thread::hardware_concurrency();
31✔
95
           return concurrency ? concurrency : 2;
31✔
96
       }()}
31✔
97
, m_current{argv + 1}
31✔
98
, m_end{argv + argc}
31✔
99
, m_charsets{
31✔
100
      []
×
101
      {
102
          const auto lowercase    = charRange('a', 'z');
31✔
103
          const auto uppercase    = charRange('A', 'Z');
31✔
104
          const auto digits       = charRange('0', '9');
31✔
105
          const auto alphanum     = lowercase | uppercase | digits;
31✔
106
          const auto printable    = charRange(' ', '~');
31✔
107
          const auto punctuation  = printable & ~alphanum;
31✔
108
          const auto bytes        = charRange('\x00', '\xff');
31✔
109
          const auto questionMark = charRange('?', '?');
31✔
110

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

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

125
    // deferred computations
126
    if (m_rawBruteforce)
28✔
127
        bruteforce = bitsetToVector(resolveCharset(*m_rawBruteforce));
12✔
128
    if (m_rawMask)
26✔
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
                {
137
                    mask->push_back({'?'});
×
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)
26✔
150
    {
151
        if (!decipheredFile && !decryptedArchive && !changePassword && !changeKeys && !bruteforce && !mask)
20✔
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)
26✔
181
        throw Error{"-c or --cipher-index parameter is missing (required by -d)"};
×
182
    if (decipheredFile && !cipherArchive && decipheredFile == cipherFile)
26✔
183
        throw Error{"-c and -d parameters must point to different files"};
×
184

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

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

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

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

203
    if (bruteforce && mask)
26✔
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
12✔
250
{
251
    return {std::max(minLength, other.minLength), std::min(maxLength, other.maxLength)};
12✔
252
}
253

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

258
    for (auto it = rawCharset.begin(); it != rawCharset.end(); ++it)
171✔
259
    {
260
        if (*it == '?') // escape character to reference other charsets
97✔
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 into m_charsets first to mark the identifier is being resolved and detect cycles
271
                if (const auto [charsetsIt, inserted] = m_charsets.try_emplace(*it); inserted)
16✔
272
                {
273
                    charsetsIt->second = resolveCharset(rawCharsetsIt->second);
14✔
274
                    m_rawCharsets.erase(rawCharsetsIt);
7✔
275
                }
276
                else
277
                    throw Error{std::string{"circular reference resolving charset ?"} + *it};
6✔
278
            }
279

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

289
    return charset;
75✔
290
}
291

292
auto Arguments::finished() const -> bool
337✔
293
{
294
    return m_current == m_end;
337✔
295
}
296

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

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

429
    return *m_current++;
454✔
430
}
431

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

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

473
#undef PAIR
474
#undef PAIRS
475

476
    const auto str = readString(description);
79✔
477
    if (const auto it = stringToOption.find(str); it == stringToOption.end())
79✔
478
        throw Error{"unknown option " + str};
×
479
    else
480
        return it->second;
158✔
481
}
110✔
482

483
auto Arguments::readInt(const std::string& description) -> int
2✔
484
{
485
    return parseInt(readString(description));
2✔
486
}
487

488
auto Arguments::readSize(const std::string& description) -> std::size_t
×
489
{
490
    return parseSize(readString(description));
×
491
}
492

493
auto Arguments::readHex(const std::string& description) -> std::vector<std::uint8_t>
1✔
494
{
495
    const auto str = readString(description);
1✔
496

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

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

506
    return data;
2✔
507
}
1✔
508

509
auto Arguments::readKey(const std::string& description) -> std::uint32_t
63✔
510
{
511
    const auto str = readString(description);
63✔
512

513
    if (str.size() > 8)
63✔
514
        throw Error{"expected a string of length 8 or less, got " + str};
×
515
    if (!std::all_of(str.begin(), str.end(), [](char c) { return std::isxdigit(static_cast<unsigned char>(c)); }))
567✔
516
        throw Error{"expected " + description + " in hexadecimal, got " + str};
×
517

518
    return static_cast<std::uint32_t>(std::stoul(str, nullptr, 16));
126✔
519
}
63✔
520

521
auto Arguments::readRawCharset(const std::string& description) -> std::string
26✔
522
{
523
    auto charset = readString(description);
26✔
524

525
    if (charset.empty())
26✔
526
        throw Error{description + " is empty"};
×
527

528
    return charset;
26✔
529
}
×
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