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

kimci86 / bkcrack / 18017127585

25 Sep 2025 06:10PM UTC coverage: 77.156% (-0.05%) from 77.205%
18017127585

push

github

kimci86
Update -r option example in readme for consistency

1557 of 2018 relevant lines covered (77.16%)

1651578.55 hits per line

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

66.04
/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
        auto       stream  = openInput(*plainArchive);
×
214
        const auto archive = Zip{stream};
×
215
        const auto entry   = plainFile ? archive[*plainFile] : archive[*plainIndex];
×
216
        Zip::checkEncryption(entry, Zip::Encryption::None);
×
217
        plaintext = archive.load(entry, plainFilePrefix);
×
218
    }
×
219
    else if (plainFile)
1✔
220
    {
221
        auto stream = openInput(*plainFile);
×
222
        plaintext   = loadStream(stream, plainFilePrefix);
×
223
    }
×
224

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

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

242
        if (!ignoreCheckByte && !extraPlaintext.count(-1))
1✔
243
        {
244
            extraPlaintextWithCheckByte        = extraPlaintext;
1✔
245
            (*extraPlaintextWithCheckByte)[-1] = entry.checkByte;
1✔
246
        }
247
    }
1✔
248
    else
249
    {
250
        auto stream = openInput(*cipherFile);
×
251
        ciphertext  = loadStream(stream, needed);
×
252
    }
×
253

254
    return {std::move(ciphertext), std::move(plaintext), offset, extraPlaintextWithCheckByte.value_or(extraPlaintext)};
2✔
255
}
1✔
256

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

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

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

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

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

297
    return charset;
75✔
298
}
299

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

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

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

437
    return *m_current++;
454✔
438
}
439

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

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

481
#undef PAIR
482
#undef PAIRS
483

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

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

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

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

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

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

514
    return data;
2✔
515
}
1✔
516

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

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

526
    return static_cast<std::uint32_t>(std::stoul(str, nullptr, 16));
126✔
527
}
63✔
528

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

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

536
    return charset;
26✔
537
}
×
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