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

randombit / botan / 15445290297

04 Jun 2025 02:40PM UTC coverage: 90.571% (+0.02%) from 90.554%
15445290297

Pull #4880

github

web-flow
Merge 54efa82ec into 6a4f830b2
Pull Request #4880: Refactor: Cipher_Mode::finish_msg() using std::span

98959 of 109261 relevant lines covered (90.57%)

12373059.89 hits per line

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

99.29
/src/lib/modes/aead/ocb/ocb.cpp
1
/*
2
* OCB Mode
3
* (C) 2013,2017 Jack Lloyd
4
* (C) 2016 Daniel Neus, Rohde & Schwarz Cybersecurity
5
*
6
* Botan is released under the Simplified BSD License (see license.txt)
7
*/
8

9
#include <botan/internal/ocb.h>
10

11
#include <botan/block_cipher.h>
12
#include <botan/mem_ops.h>
13
#include <botan/internal/bit_ops.h>
14
#include <botan/internal/ct_utils.h>
15
#include <botan/internal/poly_dbl.h>
16
#include <botan/internal/stl_util.h>
17

18
namespace Botan {
19

20
// Has to be in Botan namespace so unique_ptr can reference it
21
class L_computer final {
22
   public:
23
      explicit L_computer(const BlockCipher& cipher) :
245✔
24
            m_BS(cipher.block_size()), m_max_blocks(cipher.parallel_bytes() / m_BS) {
490✔
25
         m_L_star.resize(m_BS);
245✔
26
         cipher.encrypt(m_L_star);
245✔
27
         m_L_dollar = poly_double(star());
245✔
28

29
         // Preallocate the m_L vector to the maximum expected size to avoid
30
         // re-allocations during runtime. This had caused a use-after-free in
31
         // earlier versions, due to references into this buffer becoming stale
32
         // in `compute_offset()`, after calling `get()` in the hot path.
33
         //
34
         // Note, that the list member won't be pre-allocated, so the expected
35
         // memory overhead is negligible.
36
         //
37
         // See also https://github.com/randombit/botan/issues/3812
38
         m_L.reserve(31);
245✔
39
         m_L.push_back(poly_double(dollar()));
245✔
40

41
         while(m_L.size() < 8) {
1,960✔
42
            m_L.push_back(poly_double(m_L.back()));
3,430✔
43
         }
44

45
         m_offset_buf.resize(m_BS * m_max_blocks);
245✔
46
      }
245✔
47

48
      void init(const secure_vector<uint8_t>& offset) { m_offset = offset; }
9,457✔
49

50
      bool initialized() const { return m_offset.empty() == false; }
16,602✔
51

52
      const secure_vector<uint8_t>& star() const { return m_L_star; }
6,069✔
53

54
      const secure_vector<uint8_t>& dollar() const { return m_L_dollar; }
9,610✔
55

56
      const secure_vector<uint8_t>& offset() const { return m_offset; }
9,365✔
57

58
      const secure_vector<uint8_t>& get(size_t i) const {
121,720✔
59
         while(m_L.size() <= i) {
121,807✔
60
            m_L.push_back(poly_double(m_L.back()));
174✔
61
         }
62

63
         return m_L[i];
121,720✔
64
      }
65

66
      const uint8_t* compute_offsets(size_t block_index, size_t blocks) {
14,524✔
67
         BOTAN_ASSERT(blocks <= m_max_blocks, "OCB offsets");
14,524✔
68

69
         uint8_t* offsets = m_offset_buf.data();
14,524✔
70

71
         if(block_index % 4 == 0) {
14,524✔
72
            const secure_vector<uint8_t>& L0 = get(0);
14,070✔
73
            const secure_vector<uint8_t>& L1 = get(1);
14,070✔
74

75
            while(blocks >= 4) {
65,050✔
76
               // ntz(4*i+1) == 0
77
               // ntz(4*i+2) == 1
78
               // ntz(4*i+3) == 0
79
               block_index += 4;
36,910✔
80
               const size_t ntz4 = var_ctz32(static_cast<uint32_t>(block_index));
36,910✔
81

82
               xor_buf(offsets, m_offset.data(), L0.data(), m_BS);
36,910✔
83
               offsets += m_BS;
36,910✔
84

85
               xor_buf(offsets, offsets - m_BS, L1.data(), m_BS);
36,910✔
86
               offsets += m_BS;
36,910✔
87

88
               xor_buf(m_offset.data(), L1.data(), m_BS);
36,910✔
89
               copy_mem(offsets, m_offset.data(), m_BS);
36,910✔
90
               offsets += m_BS;
36,910✔
91

92
               xor_buf(m_offset.data(), get(ntz4).data(), m_BS);
36,910✔
93
               copy_mem(offsets, m_offset.data(), m_BS);
36,910✔
94
               offsets += m_BS;
36,910✔
95

96
               blocks -= 4;
36,910✔
97
            }
98
         }
99

100
         for(size_t i = 0; i != blocks; ++i) {  // could be done in parallel
24,164✔
101
            const size_t ntz = var_ctz32(static_cast<uint32_t>(block_index + i + 1));
9,640✔
102
            xor_buf(m_offset.data(), get(ntz).data(), m_BS);
9,640✔
103
            copy_mem(offsets, m_offset.data(), m_BS);
9,640✔
104
            offsets += m_BS;
9,640✔
105
         }
106

107
         return m_offset_buf.data();
14,524✔
108
      }
109

110
   private:
111
      static secure_vector<uint8_t> poly_double(const secure_vector<uint8_t>& in) {
2,292✔
112
         secure_vector<uint8_t> out(in.size());
2,292✔
113
         poly_double_n(out.data(), in.data(), out.size());
2,292✔
114
         return out;
2,292✔
115
      }
×
116

117
      const size_t m_BS, m_max_blocks;
118
      secure_vector<uint8_t> m_L_dollar, m_L_star;
119
      secure_vector<uint8_t> m_offset;
120
      mutable std::vector<secure_vector<uint8_t>> m_L;
121
      secure_vector<uint8_t> m_offset_buf;
122
};
123

124
namespace {
125

126
/*
127
* OCB's HASH
128
*/
129
secure_vector<uint8_t> ocb_hash(const L_computer& L, const BlockCipher& cipher, const uint8_t ad[], size_t ad_len) {
9,505✔
130
   const size_t BS = cipher.block_size();
9,505✔
131
   secure_vector<uint8_t> sum(BS);
9,505✔
132
   secure_vector<uint8_t> offset(BS);
9,505✔
133

134
   secure_vector<uint8_t> buf(BS);
9,505✔
135

136
   const size_t ad_blocks = (ad_len / BS);
9,505✔
137
   const size_t ad_remainder = (ad_len % BS);
9,505✔
138

139
   for(size_t i = 0; i != ad_blocks; ++i) {
56,535✔
140
      // this loop could run in parallel
141
      offset ^= L.get(var_ctz32(static_cast<uint32_t>(i + 1)));
94,060✔
142
      buf = offset;
47,030✔
143
      xor_buf(buf.data(), &ad[BS * i], BS);
47,030✔
144
      cipher.encrypt(buf);
47,030✔
145
      sum ^= buf;
47,030✔
146
   }
147

148
   if(ad_remainder) {
9,505✔
149
      offset ^= L.star();
5,873✔
150
      buf = offset;
5,873✔
151
      xor_buf(buf.data(), &ad[BS * ad_blocks], ad_remainder);
5,873✔
152
      buf[ad_remainder] ^= 0x80;
5,873✔
153
      cipher.encrypt(buf);
5,873✔
154
      sum ^= buf;
5,873✔
155
   }
156

157
   return sum;
9,505✔
158
}
19,010✔
159

160
}  // namespace
161

162
OCB_Mode::OCB_Mode(std::unique_ptr<BlockCipher> cipher, size_t tag_size) :
337✔
163
      m_cipher(std::move(cipher)),
337✔
164
      m_checksum(m_cipher->parallel_bytes()),
674✔
165
      m_ad_hash(m_cipher->block_size()),
337✔
166
      m_tag_size(tag_size),
337✔
167
      m_block_size(m_cipher->block_size()),
337✔
168
      m_par_blocks(m_cipher->parallel_bytes() / m_block_size) {
1,011✔
169
   const size_t BS = block_size();
337✔
170

171
   /*
172
   * draft-krovetz-ocb-wide-d1 specifies OCB for several other block
173
   * sizes but only 128, 192, 256 and 512 bit are currently supported
174
   * by this implementation.
175
   */
176
   BOTAN_ARG_CHECK(BS == 16 || BS == 24 || BS == 32 || BS == 64, "Invalid block size for OCB");
337✔
177

178
   BOTAN_ARG_CHECK(m_tag_size % 4 == 0 && m_tag_size >= 8 && m_tag_size <= BS && m_tag_size <= 32,
337✔
179
                   "Invalid OCB tag length");
180
}
337✔
181

182
OCB_Mode::~OCB_Mode() = default;
1,991✔
183

184
void OCB_Mode::clear() {
92✔
185
   m_cipher->clear();
92✔
186
   m_L.reset();  // add clear here?
92✔
187
   reset();
92✔
188
}
92✔
189

190
void OCB_Mode::reset() {
410✔
191
   m_block_index = 0;
410✔
192
   zeroise(m_ad_hash);
410✔
193
   zeroise(m_checksum);
410✔
194
   m_last_nonce.clear();
410✔
195
   m_stretch.clear();
410✔
196
}
410✔
197

198
bool OCB_Mode::valid_nonce_length(size_t length) const {
9,503✔
199
   if(length == 0) {
9,503✔
200
      return false;
201
   }
202
   if(block_size() == 16) {
9,503✔
203
      return length < 16;
7,901✔
204
   } else {
205
      return length < (block_size() - 1);
1,602✔
206
   }
207
}
208

209
std::string OCB_Mode::name() const {
506✔
210
   return m_cipher->name() + "/OCB";  // include tag size?
1,012✔
211
}
212

213
size_t OCB_Mode::update_granularity() const {
1,689✔
214
   return block_size();
1,689✔
215
}
216

217
size_t OCB_Mode::ideal_granularity() const {
276✔
218
   return (m_par_blocks * block_size());
276✔
219
}
220

221
Key_Length_Specification OCB_Mode::key_spec() const {
245✔
222
   return m_cipher->key_spec();
245✔
223
}
224

225
bool OCB_Mode::has_keying_material() const {
36,346✔
226
   return m_cipher->has_keying_material();
36,346✔
227
}
228

229
void OCB_Mode::key_schedule(std::span<const uint8_t> key) {
245✔
230
   m_cipher->set_key(key);
245✔
231
   m_L = std::make_unique<L_computer>(*m_cipher);
245✔
232
}
245✔
233

234
void OCB_Mode::set_associated_data_n(size_t idx, std::span<const uint8_t> ad) {
9,689✔
235
   BOTAN_ARG_CHECK(idx == 0, "OCB: cannot handle non-zero index in set_associated_data_n");
9,689✔
236
   assert_key_material_set();
9,689✔
237
   m_ad_hash = ocb_hash(*m_L, *m_cipher, ad.data(), ad.size());
9,505✔
238
}
9,505✔
239

240
const secure_vector<uint8_t>& OCB_Mode::update_nonce(const uint8_t nonce[], size_t nonce_len) {
9,457✔
241
   const size_t BS = block_size();
9,457✔
242

243
   BOTAN_ASSERT(BS == 16 || BS == 24 || BS == 32 || BS == 64, "OCB block size is supported");
9,457✔
244

245
   // NOLINTNEXTLINE(readability-avoid-nested-conditional-operator)
246
   const size_t MASKLEN = (BS == 16 ? 6 : ((BS == 24) ? 7 : 8));
9,457✔
247

248
   const uint8_t BOTTOM_MASK = static_cast<uint8_t>((static_cast<uint16_t>(1) << MASKLEN) - 1);
9,457✔
249

250
   m_nonce_buf.resize(BS);
9,457✔
251
   clear_mem(&m_nonce_buf[0], m_nonce_buf.size());
9,457✔
252

253
   copy_mem(&m_nonce_buf[BS - nonce_len], nonce, nonce_len);
9,457✔
254
   m_nonce_buf[0] = static_cast<uint8_t>(((tag_size() * 8) % (BS * 8)) << (BS <= 16 ? 1 : 0));
11,055✔
255

256
   m_nonce_buf[BS - nonce_len - 1] ^= 1;
9,457✔
257

258
   const uint8_t bottom = m_nonce_buf[BS - 1] & BOTTOM_MASK;
9,457✔
259
   m_nonce_buf[BS - 1] &= ~BOTTOM_MASK;
9,457✔
260

261
   const bool need_new_stretch = (m_last_nonce != m_nonce_buf);
9,457✔
262

263
   if(need_new_stretch) {
9,457✔
264
      m_last_nonce = m_nonce_buf;
683✔
265

266
      m_cipher->encrypt(m_nonce_buf);
683✔
267

268
      /*
269
      The loop bounds (BS vs BS/2) are derived from the relation
270
      between the block size and the MASKLEN. Using the terminology
271
      of draft-krovetz-ocb-wide, we have to derive enough bits in
272
      ShiftedKtop to read up to BLOCKLEN+bottom bits from Stretch.
273

274
                 +----------+---------+-------+---------+
275
                 | BLOCKLEN | RESIDUE | SHIFT | MASKLEN |
276
                 +----------+---------+-------+---------+
277
                 |       32 |     141 |    17 |    4    |
278
                 |       64 |      27 |    25 |    5    |
279
                 |       96 |    1601 |    33 |    6    |
280
                 |      128 |     135 |     8 |    6    |
281
                 |      192 |     135 |    40 |    7    |
282
                 |      256 |    1061 |     1 |    8    |
283
                 |      384 |    4109 |    80 |    8    |
284
                 |      512 |     293 |   176 |    8    |
285
                 |     1024 |  524355 |   352 |    9    |
286
                 +----------+---------+-------+---------+
287
      */
288
      if(BS == 16) {
683✔
289
         for(size_t i = 0; i != BS / 2; ++i) {
5,535✔
290
            m_nonce_buf.push_back(m_nonce_buf[i] ^ m_nonce_buf[i + 1]);
4,920✔
291
         }
292
      } else if(BS == 24) {
68✔
293
         for(size_t i = 0; i != 16; ++i) {
408✔
294
            m_nonce_buf.push_back(m_nonce_buf[i] ^ m_nonce_buf[i + 5]);
384✔
295
         }
296
      } else if(BS == 32) {
44✔
297
         for(size_t i = 0; i != BS; ++i) {
1,089✔
298
            m_nonce_buf.push_back(m_nonce_buf[i] ^ (m_nonce_buf[i] << 1) ^ (m_nonce_buf[i + 1] >> 7));
1,056✔
299
         }
300
      } else if(BS == 64) {
301
         for(size_t i = 0; i != BS / 2; ++i) {
363✔
302
            m_nonce_buf.push_back(m_nonce_buf[i] ^ m_nonce_buf[i + 22]);
352✔
303
         }
304
      }
305

306
      m_stretch = m_nonce_buf;
683✔
307
   }
308

309
   // now set the offset from stretch and bottom
310
   const size_t shift_bytes = bottom / 8;
9,457✔
311
   const size_t shift_bits = bottom % 8;
9,457✔
312

313
   BOTAN_ASSERT(m_stretch.size() >= BS + shift_bytes + 1, "Size ok");
9,457✔
314

315
   m_offset.resize(BS);
9,457✔
316
   for(size_t i = 0; i != BS; ++i) {
195,705✔
317
      m_offset[i] = (m_stretch[i + shift_bytes] << shift_bits);
186,248✔
318
      m_offset[i] |= (m_stretch[i + shift_bytes + 1] >> (8 - shift_bits));
186,248✔
319
   }
320

321
   return m_offset;
9,457✔
322
}
323

324
void OCB_Mode::start_msg(const uint8_t nonce[], size_t nonce_len) {
9,457✔
325
   if(!valid_nonce_length(nonce_len)) {
9,457✔
326
      throw Invalid_IV_Length(name(), nonce_len);
×
327
   }
328

329
   assert_key_material_set();
9,457✔
330

331
   m_L->init(update_nonce(nonce, nonce_len));
9,457✔
332
   zeroise(m_checksum);
9,457✔
333
   m_block_index = 0;
9,457✔
334
}
9,457✔
335

336
void OCB_Encryption::encrypt(uint8_t buffer[], size_t blocks) {
4,199✔
337
   assert_key_material_set();
4,199✔
338
   BOTAN_STATE_CHECK(m_L->initialized());
4,153✔
339

340
   const size_t BS = block_size();
4,107✔
341

342
   while(blocks) {
11,781✔
343
      const size_t proc_blocks = std::min(blocks, par_blocks());
7,674✔
344
      const size_t proc_bytes = proc_blocks * BS;
7,674✔
345

346
      const uint8_t* offsets = m_L->compute_offsets(m_block_index, proc_blocks);
7,674✔
347

348
      xor_buf(m_checksum.data(), buffer, proc_bytes);
7,674✔
349

350
      xor_buf(buffer, offsets, proc_bytes);
7,674✔
351
      m_cipher->encrypt_n(buffer, buffer, proc_blocks);
7,674✔
352
      xor_buf(buffer, offsets, proc_bytes);
7,674✔
353

354
      buffer += proc_bytes;
7,674✔
355
      blocks -= proc_blocks;
7,674✔
356
      m_block_index += proc_blocks;
7,674✔
357
   }
358
}
4,107✔
359

360
size_t OCB_Encryption::process_msg(uint8_t buf[], size_t sz) {
500✔
361
   BOTAN_ARG_CHECK(sz % update_granularity() == 0, "Invalid OCB input size");
500✔
362
   encrypt(buf, sz / block_size());
500✔
363
   return sz;
408✔
364
}
365

366
size_t OCB_Encryption::finish_msg(std::span<uint8_t> buffer, size_t input_bytes) {
5,708✔
367
   assert_key_material_set();
5,708✔
368
   BOTAN_STATE_CHECK(m_L->initialized());
5,616✔
369

370
   const size_t BS = block_size();
5,570✔
371
   const size_t tag_length = tag_size();
5,570✔
372

373
   BOTAN_ASSERT_NOMSG(input_bytes + tag_length == buffer.size());
5,570✔
374

375
   const auto remaining = buffer.first(input_bytes);
5,570✔
376
   const auto tag = buffer.last(tag_length);
5,570✔
377

378
   secure_vector<uint8_t> mac(BS);
5,570✔
379

380
   if(!remaining.empty()) {
5,570✔
381
      const auto final_full_block_count = remaining.size() / BS;
3,699✔
382
      const auto final_full_blocks = remaining.first(final_full_block_count * BS);
3,699✔
383
      const auto remainder = remaining.subspan(final_full_blocks.size());
3,699✔
384

385
      encrypt(final_full_blocks.data(), final_full_block_count);
3,699✔
386
      copy_mem(mac, m_L->offset());
3,699✔
387

388
      if(!remainder.empty()) {
3,699✔
389
         BOTAN_ASSERT(remainder.size() < BS, "Only a partial block left");
3,495✔
390

391
         xor_buf(std::span{m_checksum}.first(remainder.size()), remainder);
3,495✔
392
         m_checksum[remainder.size()] ^= 0x80;
3,495✔
393

394
         // Offset_*
395
         xor_buf(mac, m_L->star());
3,495✔
396

397
         secure_vector<uint8_t> pad(BS);
3,495✔
398
         m_cipher->encrypt(mac, pad);
3,495✔
399
         xor_buf(remainder, std::span{pad}.first(remainder.size()));
3,495✔
400
      }
3,495✔
401
   } else {
402
      copy_mem(mac, m_L->offset());
1,871✔
403
   }
404

405
   // now compute the tag
406

407
   // fold checksum
408
   BufferSlicer cs(m_checksum);
5,570✔
409
   while(cs.remaining() >= BS) {
130,954✔
410
      xor_buf(mac, cs.take(BS));
125,384✔
411
   }
412
   BOTAN_ASSERT_NOMSG(cs.empty());
5,570✔
413

414
   xor_buf(mac, m_L->dollar());
5,570✔
415
   m_cipher->encrypt(mac);
5,570✔
416
   xor_buf(mac, m_ad_hash);
5,570✔
417

418
   copy_mem(tag, std::span{mac}.first(tag.size()));
5,570✔
419

420
   zeroise(m_checksum);
5,570✔
421
   m_block_index = 0;
5,570✔
422

423
   return buffer.size();
5,570✔
424
}
14,635✔
425

426
void OCB_Decryption::decrypt(uint8_t buffer[], size_t blocks) {
3,038✔
427
   assert_key_material_set();
3,038✔
428
   BOTAN_STATE_CHECK(m_L->initialized());
2,992✔
429

430
   const size_t BS = block_size();
2,946✔
431

432
   while(blocks) {
9,796✔
433
      const size_t proc_blocks = std::min(blocks, par_blocks());
6,850✔
434
      const size_t proc_bytes = proc_blocks * BS;
6,850✔
435

436
      const uint8_t* offsets = m_L->compute_offsets(m_block_index, proc_blocks);
6,850✔
437

438
      xor_buf(buffer, offsets, proc_bytes);
6,850✔
439
      m_cipher->decrypt_n(buffer, buffer, proc_blocks);
6,850✔
440
      xor_buf(buffer, offsets, proc_bytes);
6,850✔
441

442
      xor_buf(m_checksum.data(), buffer, proc_bytes);
6,850✔
443

444
      buffer += proc_bytes;
6,850✔
445
      blocks -= proc_blocks;
6,850✔
446
      m_block_index += proc_blocks;
6,850✔
447
   }
448
}
2,946✔
449

450
size_t OCB_Decryption::process_msg(uint8_t buf[], size_t sz) {
513✔
451
   BOTAN_ARG_CHECK(sz % update_granularity() == 0, "Invalid OCB input size");
513✔
452
   decrypt(buf, sz / block_size());
513✔
453
   return sz;
421✔
454
}
455

456
size_t OCB_Decryption::finish_msg(std::span<uint8_t> buffer, size_t input_bytes) {
3,933✔
457
   assert_key_material_set();
3,933✔
458
   BOTAN_STATE_CHECK(m_L->initialized());
3,841✔
459

460
   const size_t BS = block_size();
3,795✔
461
   const auto tag_length = tag_size();
3,795✔
462

463
   BOTAN_ASSERT_NOMSG(buffer.size() == input_bytes);
3,795✔
464
   BOTAN_ASSERT_NOMSG(buffer.size() >= tag_length);
3,795✔
465

466
   const auto remaining = buffer.first(buffer.size() - tag_length);
3,795✔
467
   const auto tag = buffer.last(tag_length);
3,795✔
468

469
   secure_vector<uint8_t> mac(BS);
3,795✔
470

471
   if(!remaining.empty()) {
3,795✔
472
      const auto final_full_block_count = remaining.size() / BS;
2,525✔
473
      const auto final_full_blocks = remaining.first(final_full_block_count * BS);
2,525✔
474
      const auto remainder = remaining.subspan(final_full_blocks.size());
2,525✔
475

476
      decrypt(final_full_blocks.data(), final_full_block_count);
2,525✔
477
      copy_mem(mac, m_L->offset());
2,525✔
478

479
      if(!remainder.empty()) {
2,525✔
480
         BOTAN_ASSERT(remainder.size() < BS, "Only a partial block left");
2,329✔
481

482
         xor_buf(mac, m_L->star());
2,329✔
483
         secure_vector<uint8_t> pad(BS);
2,329✔
484
         m_cipher->encrypt(mac, pad);  // P_*
2,329✔
485
         xor_buf(remainder, std::span{pad}.first(remainder.size()));
2,329✔
486

487
         xor_buf(std::span{m_checksum}.first(remainder.size()), remainder);
2,329✔
488
         m_checksum[remainder.size()] ^= 0x80;
2,329✔
489
      }
2,329✔
490
   } else {
491
      copy_mem(mac, m_L->offset());
1,270✔
492
   }
493

494
   // compute the mac
495

496
   // fold checksum
497
   BufferSlicer cs(m_checksum);
3,795✔
498
   while(cs.remaining() >= BS) {
124,391✔
499
      xor_buf(mac, cs.take(BS));
120,734✔
500
   }
501
   BOTAN_ASSERT_NOMSG(cs.empty());
3,795✔
502

503
   xor_buf(mac, m_L->dollar());
3,795✔
504
   m_cipher->encrypt(mac);
3,795✔
505
   xor_buf(mac, m_ad_hash);
3,795✔
506

507
   // reset state
508
   zeroise(m_checksum);
3,795✔
509
   m_block_index = 0;
3,795✔
510

511
   // compare mac
512
   if(!CT::is_equal(mac.data(), tag.data(), tag.size()).as_bool()) {
7,590✔
513
      throw Invalid_Authentication_Tag("OCB tag check failed");
138✔
514
   }
515

516
   // remove tag from end of message
517
   return remaining.size();
3,657✔
518
}
3,657✔
519

520
}  // namespace Botan
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