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

kimci86 / bkcrack / 28544354086

01 Jul 2026 08:02PM UTC coverage: 93.121% (+0.02%) from 93.103%
28544354086

push

github

kimci86
Take advantage of actions/upload-artifact@v7 to avoid zipping packages

1868 of 2006 relevant lines covered (93.12%)

3083021.52 hits per line

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

91.02
/src/cli/Arguments.cpp
1
#include "Arguments.hpp"
2

3
#include <bkcrack/Zip.hpp>
4
#include <bkcrack/file.hpp>
5

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

13
namespace
14
{
15

16
auto charRange(std::uint8_t first, std::uint8_t last) -> std::bitset<256>
528✔
17
{
18
    auto bitset = std::bitset<256>{};
1,584✔
19

20
    do
21
    {
22
        bitset.set(first);
36,432✔
23
    } while (first++ != last);
36,432✔
24

25
    return bitset;
528✔
26
}
27

28
auto bitsetToVector(const std::bitset<256>& charset) -> std::vector<std::uint8_t>
89✔
29
{
30
    auto vector = std::vector<std::uint8_t>{};
89✔
31
    for (auto c = 0; c < 256; c++)
22,873✔
32
        if (charset[c])
22,784✔
33
            vector.push_back(c);
3,306✔
34

35
    return vector;
89✔
36
}
×
37

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

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

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

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

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

73
        if (0 < minEnd)
6✔
74
            interval.minLength = parseSize(value.substr(0, minEnd));
4✔
75

76
        if (const auto maxBegin = minEnd + separator.size(); maxBegin < value.size())
6✔
77
            interval.maxLength = parseSize(value.substr(maxBegin));
4✔
78

79
        return interval;
6✔
80
    }
81
    else
82
        return parseSize(value);
15✔
83
}
21✔
84

85
} // namespace
86

87
Arguments::Error::Error(const std::string& description)
28✔
88
: BaseError{"Arguments error", description}
84✔
89
{
90
}
28✔
91

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

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

123
    if (help || version || infoArchive)
82✔
124
        return; // no further checks are needed for those options
7✔
125

126
    // deferred computations
127
    if (m_rawBruteforce)
75✔
128
        bruteforce = bitsetToVector(resolveCharset(*m_rawBruteforce));
25✔
129
    if (m_rawMask)
71✔
130
    {
131
        mask.emplace();
13✔
132
        for (auto it = m_rawMask->begin(); it != m_rawMask->end(); ++it)
185✔
133
        {
134
            if (*it == '?') // escape character to reference other charsets
172✔
135
            {
136
                if (++it == m_rawMask->end())
68✔
137
                {
138
                    mask->push_back({'?'});
×
139
                    break;
×
140
                }
141

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

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

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

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

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

175
        constexpr auto minimumOffset = -static_cast<int>(Data::encryptionHeaderSize);
13✔
176
        if (offset < minimumOffset)
13✔
177
            throw Error{"plaintext offset " + std::to_string(offset) + " is too small (minimum is " +
2✔
178
                        std::to_string(minimumOffset) + ")"};
3✔
179
        if (!extraPlaintext.empty() && extraPlaintext.begin()->first < minimumOffset)
12✔
180
            throw Error{"extra plaintext offset " + std::to_string(extraPlaintext.begin()->first) +
2✔
181
                        " is too small (minimum is " + std::to_string(minimumOffset) + ")"};
3✔
182
    }
183

184
    if (decipheredFile && !cipherFile && !cipherIndex)
60✔
185
        throw Error{"-c or --cipher-index parameter is missing (required by -d)"};
3✔
186
    if (decipheredFile && !cipherArchive && decipheredFile == cipherFile)
59✔
187
        throw Error{"-c and -d parameters must point to different files"};
×
188

189
    if (decryptedArchive && !cipherArchive)
59✔
190
        throw Error{"-C parameter is missing (required by -D)"};
3✔
191
    if (decryptedArchive && decryptedArchive == cipherArchive)
58✔
192
        throw Error{"-C and -D parameters must point to different files"};
×
193

194
    if (changePassword && !cipherArchive)
58✔
195
        throw Error{"-C parameter is missing (required by -U)"};
3✔
196
    if (changePassword && changePassword->unlockedArchive == cipherArchive)
57✔
197
        throw Error{"-C and -U parameters must point to different files"};
×
198

199
    if (changeKeys && !cipherArchive)
57✔
200
        throw Error{"-C parameter is missing (required by --change-keys)"};
3✔
201
    if (changeKeys && changeKeys->unlockedArchive == cipherArchive)
56✔
202
        throw Error{"-C and --change-keys parameters must point to different files"};
×
203

204
    if (length && !bruteforce)
56✔
205
        throw Error{"--bruteforce parameter is missing (required by --length)"};
3✔
206

207
    if (bruteforce && mask)
55✔
208
        throw Error{"--bruteforce and --mask cannot be used at the same time"};
3✔
209
}
486✔
210

211
auto Arguments::loadData() const -> Data
1✔
212
{
213
    // load known plaintext
214
    auto plaintext = std::vector<std::uint8_t>{};
1✔
215
    if (plainArchive)
1✔
216
    {
217
        auto       stream  = openInput(*plainArchive);
×
218
        const auto archive = Zip{stream};
×
219
        const auto entry   = plainFile ? archive[*plainFile] : archive[*plainIndex];
×
220
        Zip::checkEncryption(entry, Zip::Encryption::None);
×
221
        plaintext = archive.load(entry, plainFilePrefix);
×
222
    }
×
223
    else if (plainFile)
1✔
224
    {
225
        auto stream = openInput(*plainFile);
×
226
        plaintext   = loadStream(stream, plainFilePrefix);
×
227
    }
×
228

229
    // load ciphertext needed by the attack
230
    auto needed = Data::encryptionHeaderSize;
1✔
231
    if (!plaintext.empty())
1✔
232
        needed = std::max(needed, Data::encryptionHeaderSize + offset + plaintext.size());
×
233
    if (!extraPlaintext.empty())
1✔
234
        needed = std::max(needed, Data::encryptionHeaderSize + extraPlaintext.rbegin()->first + 1);
1✔
235

236
    auto ciphertext = std::vector<std::uint8_t>{};
1✔
237
    auto checkByte  = std::optional<std::uint8_t>{};
1✔
238
    if (cipherArchive)
1✔
239
    {
240
        auto       stream  = openInput(*cipherArchive);
1✔
241
        const auto archive = Zip{stream};
1✔
242
        const auto entry   = cipherFile ? archive[*cipherFile] : archive[*cipherIndex];
1✔
243
        Zip::checkEncryption(entry, Zip::Encryption::Traditional);
1✔
244
        ciphertext = archive.load(entry, needed);
1✔
245

246
        if (!ignoreCheckByte)
1✔
247
            checkByte = entry.checkByte;
1✔
248
    }
1✔
249
    else
250
    {
251
        auto stream = openInput(*cipherFile);
×
252
        ciphertext  = loadStream(stream, needed);
×
253
    }
×
254

255
    return {std::move(ciphertext), checkByte, std::move(plaintext), offset, extraPlaintext};
2✔
256
}
1✔
257

258
auto Arguments::LengthInterval::operator&(const Arguments::LengthInterval& other) const -> Arguments::LengthInterval
21✔
259
{
260
    return {std::max(minLength, other.minLength), std::min(maxLength, other.maxLength)};
21✔
261
}
262

263
auto Arguments::resolveCharset(const std::string& rawCharset) -> std::bitset<256>
111✔
264
{
265
    auto charset = std::bitset<256>{};
333✔
266

267
    for (auto it = rawCharset.begin(); it != rawCharset.end(); ++it)
226✔
268
    {
269
        if (*it == '?') // escape character to reference other charsets
129✔
270
        {
271
            if (++it == rawCharset.end())
111✔
272
            {
273
                charset.set('?');
1✔
274
                break;
1✔
275
            }
276

277
            if (const auto rawCharsetsIt = m_rawCharsets.find(*it); rawCharsetsIt != m_rawCharsets.end())
110✔
278
            {
279
                // insert into m_charsets first to mark the identifier is being resolved and detect cycles
280
                if (const auto [charsetsIt, inserted] = m_charsets.try_emplace(*it); inserted)
21✔
281
                {
282
                    charsetsIt->second = resolveCharset(rawCharsetsIt->second);
18✔
283
                    m_rawCharsets.erase(rawCharsetsIt);
9✔
284
                }
285
                else
286
                    throw Error{std::string{"circular reference resolving charset ?"} + *it};
9✔
287
            }
288

289
            if (const auto charsetsIt = m_charsets.find(*it); charsetsIt != m_charsets.end())
98✔
290
                charset |= charsetsIt->second;
97✔
291
            else
292
                throw Error{std::string{"unknown charset ?"} + *it};
3✔
293
        }
294
        else
295
            charset.set(*it);
18✔
296
    }
297

298
    return charset;
98✔
299
}
300

301
auto Arguments::finished() const -> bool
870✔
302
{
303
    return m_current == m_end;
870✔
304
}
305

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

433
auto Arguments::readString(const std::string& description) -> std::string
571✔
434
{
435
    if (finished())
571✔
436
        throw Error{"expected " + description + ", got nothing"};
×
437

438
    return *m_current++;
1,142✔
439
}
440

441
auto Arguments::readOption(const std::string& description) -> Arguments::Option
217✔
442
{
443
    // clang-format off
444
#define PAIR(string, option) {#string, Option::option}
445
#define PAIRS(short, long, option) PAIR(short, option), PAIR(long, option)
446

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

482
#undef PAIR
483
#undef PAIRS
484

485
    const auto str = readString(description);
217✔
486
    if (const auto it = stringToOption.find(str); it == stringToOption.end())
217✔
487
        throw Error{"unknown option " + str};
×
488
    else
489
        return it->second;
434✔
490
}
249✔
491

492
auto Arguments::readInt(const std::string& description) -> int
14✔
493
{
494
    return parseInt(readString(description));
14✔
495
}
496

497
auto Arguments::readSize(const std::string& description) -> std::size_t
7✔
498
{
499
    return parseSize(readString(description));
7✔
500
}
501

502
auto Arguments::readHex(const std::string& description) -> std::vector<std::uint8_t>
8✔
503
{
504
    const auto str = readString(description);
8✔
505

506
    if (str.size() % 2)
8✔
507
        throw Error{"expected an even-length string, got " + str};
1✔
508
    if (!std::ranges::all_of(str, [](char c) { return std::isxdigit(static_cast<unsigned char>(c)); }))
74✔
509
        throw Error{"expected " + description + " in hexadecimal, got " + str};
1✔
510

511
    auto data = std::vector<std::uint8_t>{};
6✔
512
    for (auto i = std::size_t{}; i < str.length(); i += 2)
39✔
513
        data.push_back(static_cast<std::uint8_t>(std::stoul(str.substr(i, 2), nullptr, 16)));
33✔
514

515
    return data;
12✔
516
}
8✔
517

518
auto Arguments::readKey(const std::string& description) -> std::uint32_t
149✔
519
{
520
    const auto str = readString(description);
149✔
521

522
    if (str.size() > 8)
149✔
523
        throw Error{"expected a string of length 8 or less, got " + str};
1✔
524
    if (!std::ranges::all_of(str, [](char c) { return std::isxdigit(static_cast<unsigned char>(c)); }))
827✔
525
        throw Error{"expected " + description + " in hexadecimal, got " + str};
1✔
526

527
    return static_cast<std::uint32_t>(std::stoul(str, nullptr, 16));
294✔
528
}
149✔
529

530
auto Arguments::readRawCharset(const std::string& description) -> std::string
43✔
531
{
532
    auto charset = readString(description);
43✔
533

534
    if (charset.empty())
43✔
535
        throw Error{description + " is empty"};
×
536

537
    return charset;
43✔
538
}
×
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