File size: 34,925 Bytes
049224b
 
 
 
 
698e6bf
 
049224b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3606bf
 
 
 
 
 
 
 
1f20048
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
949aa2a
 
 
 
 
 
 
 
 
 
e3606bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
049224b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698e6bf
 
049224b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698e6bf
 
 
76be973
698e6bf
 
 
76be973
049224b
 
 
698e6bf
76be973
 
 
 
698e6bf
76be973
 
 
 
 
 
698e6bf
 
 
 
76be973
 
 
698e6bf
76be973
 
 
 
698e6bf
76be973
698e6bf
76be973
698e6bf
 
 
1b860c4
 
 
93427d6
1b860c4
76be973
 
 
 
698e6bf
76be973
 
 
 
698e6bf
76be973
 
 
 
 
 
698e6bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
049224b
 
 
 
698e6bf
049224b
 
 
698e6bf
049224b
 
 
 
 
 
 
 
 
 
698e6bf
 
 
049224b
 
 
 
 
 
1f20048
 
 
 
 
 
 
 
 
 
 
 
049224b
1f20048
 
 
 
 
 
 
 
 
 
049224b
 
 
1f20048
049224b
 
 
 
 
949aa2a
 
 
 
 
 
 
049224b
949aa2a
049224b
949aa2a
049224b
 
 
949aa2a
049224b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698e6bf
 
 
049224b
 
698e6bf
049224b
 
 
 
 
 
 
 
 
698e6bf
 
 
049224b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
949aa2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
049224b
949aa2a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
049224b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
e3606bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
949aa2a
 
 
 
e3606bf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
049224b
 
e3606bf
049224b
 
 
e3606bf
 
 
049224b
 
 
949aa2a
 
 
 
049224b
 
949aa2a
 
 
 
 
e3606bf
 
 
 
 
 
 
 
 
 
049224b
e3606bf
 
 
049224b
949aa2a
e3606bf
 
 
 
049224b
 
 
 
 
 
 
 
 
 
 
 
e3606bf
1f20048
949aa2a
e3606bf
 
049224b
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
/*
 * aead.js — AEAD usage-limit math engine.
 *
 * Implements the advantage bounds and usage limits from
 * draft-irtf-cfrg-aead-limits (single-key limits in Section 6,
 * multi-key limits in Section 7), plus the claim-based limits from the
 * AEGIS specification and analyses cited by draft-irtf-cfrg-aegis-aead.
 *
 * All quantities are handled as log2 values, so limits like 2^64.5
 * stay exactly representable in double precision. Every function takes
 * and returns plain numbers on the log2 scale (Infinity means
 * "no limit", NaN means "no feasible value").
 *
 * Works both in the browser (exposed as window.AEADLimits) and under
 * Node.js (module.exports) so the self-test harness can run headless.
 */
(function (global) {
  'use strict';

  var LOG2_10 = Math.log2(10);

  /* ---------- small log2-domain helpers ---------- */

  // log2(2^a + 2^b); b may be -Infinity.
  function log2sum(a, b) {
    if (b === -Infinity || b === undefined) return a;
    if (a < b) { var t = a; a = b; b = t; }
    return a + Math.log2(1 + Math.pow(2, b - a));
  }

  // log2(2^a - 2^b), requires a > b.
  function log2diff(a, b) {
    if (b === -Infinity) return a;
    if (a <= b) return -Infinity;
    return a + Math.log2(1 - Math.pow(2, b - a));
  }

  // Smallest of the given log2 values (ignoring undefined).
  function minLog2() {
    var m = Infinity;
    for (var i = 0; i < arguments.length; i++) {
      var x = arguments[i];
      if (x === undefined) continue;
      if (x < m) m = x;
    }
    return m;
  }

  // Solve a*x^2 + b*x = C for x > 0, with a >= 0, b >= 0, C > 0,
  // given la = log2(a) (-Infinity if a = 0), lb = log2(b) (-Infinity if b = 0),
  // lC = log2(C). Returns log2(x).
  function solveQuadUsage(la, lb, lC) {
    if (la === -Infinity) return lC - lb;          // b*x = C
    if (lb === -Infinity) return (lC - la) / 2;    // a*x^2 = C
    // x = (sqrt(b^2 + 4aC) - b) / (2a) = 2C / (sqrt(b^2 + 4aC) + b)
    var disc = log2sum(2 * lb, la + lC + 2);       // log2(b^2 + 4aC)
    var denom = log2sum(disc, lb) + 1;             // log2(2*(sqrt(...)+b))
    return lC - denom;
  }

  // log2(2^e - 1), evaluated without losing precision when e is small.
  function log2PowMinusOne(e) {
    if (e === Infinity) return Infinity;
    if (e <= 0) return -Infinity;
    if (e > 53) return e;
    return e + Math.log1p(-Math.pow(2, -e)) / Math.LN2;
  }

  /*
   * log2(delta_n(a)) from Iwata–Ohashi–Minematsu, Equation (22):
   *
   *   delta_n(a) = (1 - (a - 1)/2^n)^(-a/2).
   *
   * lA is log2(a). Math.expm1/log1p preserve the small correction when
   * a is far below 2^n, which is the usual GCM operating range.
   */
  function log2GcmDelta(lA, n) {
    if (lA === -Infinity) return 0;
    if (!isFinite(lA) || lA >= n) return Infinity;
    var a = Math.pow(2, lA);
    var x = Math.expm1(lA * Math.LN2) * Math.pow(2, -n);
    if (x <= 0) return 0;
    if (x >= 1) return Infinity;
    return -(a / 2) * Math.log1p(-x) / Math.LN2;
  }

  /*
   * Maximum |AAD|_128 + |plaintext|_128 when only their combined byte length
   * is bounded. Poly1305 pads the two strings separately, so the worst split
   * can consume one more data block than ceil((|AAD| + |plaintext|) / 16).
   */
  function maxPoly1305DataBlocks(lBytes) {
    var bytes = Math.pow(2, lBytes);
    return Math.ceil((bytes + 15) / 16);
  }

  /*
   * Union bound on a same-key collision among independently uniform nonces.
   *
   * In the single-key setting, lM is Infinity and the bound is
   *
   *   q(q-1) / 2^(r+1).
   *
   * In the multi-key setting, q is the total number of protected messages
   * and M is the maximum under any one key.  If q_i is the number under key
   * i, then
   *
   *   sum_i q_i(q_i-1) / 2^(r+1)
   *     <= q(min(q,M)-1) / 2^(r+1).
   *
   * The min(q,M) makes the bound reduce to the single-key expression when
   * the aggregate usage has not yet reached M.  A missing lM deliberately
   * defaults to Infinity, which is the safe worst case where all messages
   * might use one key.
   */
  function randomNonceCollision(lq, nonceBits, lM) {
    if (lq === -Infinity) return -Infinity;
    if (lq === Infinity) return Infinity;
    var maxPerKey = lM === undefined ? Infinity : Math.max(0, lM);
    var pairWidth = Math.min(lq, maxPerKey);
    if (lq <= 0 || pairWidth <= 0) return -Infinity;
    return lq + log2PowMinusOne(pairWidth) - (nonceBits + 1);
  }

  // Invert randomNonceCollision on the log2(message-count) axis.
  function randomNonceLimit(lP, nonceBits, lM) {
    var maxPerKey = lM === undefined ? Infinity : Math.max(0, lM);
    if (maxPerKey === 0) return Infinity; // at most one message per key
    var lo = 0;
    var hi = Math.max(1, nonceBits + 2);
    // randomNonceCollision is monotone.  The fixed high endpoint is beyond
    // every meaningful root for the nonce sizes exposed by this module.
    for (var i = 0; i < 100; i++) {
      var mid = (lo + hi) / 2;
      if (randomNonceCollision(mid, nonceBits, maxPerKey) <= lP) lo = mid;
      else hi = mid;
    }
    return lo;
  }

  /* ---------- display formatting ---------- */

  function fmtPow(e, digits) {
    if (e === Infinity) return '∞';
    if (e === -Infinity) return '0';
    if (isNaN(e)) return 'n/a';
    var d = digits === undefined ? 1 : digits;
    if (Math.abs(e - Math.round(e)) < 1e-9) e = Math.round(e);
    return '2^' + e.toFixed(d).replace(/\.0+$/, '').replace(/(\.\d*?)0+$/, '$1');
  }

  function fmtNum(e) {
    if (e === Infinity) return '∞ (no limit)';
    if (e === -Infinity) return '0';
    if (isNaN(e)) return 'n/a';
    if (e < 0) return Math.pow(2, e).toExponential(2);
    if (e <= 64) {
      var v = Math.pow(2, e);
      return Math.round(v).toLocaleString('en-US');
    }
    // log10(2^e) = e / log2(10).
    var e10 = e / LOG2_10;
    var k = Math.floor(e10);
    var m = Math.pow(10, e10 - k);
    return m.toFixed(2) + ' × 10^' + k;
  }

  function fmtShort(e) {
    if (e === Infinity) return '∞';
    if (e === -Infinity) return '0';
    if (isNaN(e)) return 'n/a';
    if (e <= 30) return Math.round(Math.pow(2, e)).toLocaleString('en-US');
    return fmtPow(e, 1);
  }

  /* ---------- algorithm catalogue ---------- */

  var ALGORITHMS = {
    AEAD_AES_128_GCM: {
      name: 'AEAD_AES_128_GCM', family: 'GCM', k: 128, n: 128, t: 128, r: 96,
      blockBytes: 16
    },
    AEAD_AES_256_GCM: {
      name: 'AEAD_AES_256_GCM', family: 'GCM', k: 256, n: 128, t: 128, r: 96,
      blockBytes: 16
    },
    AEAD_CHACHA20_POLY1305: {
      name: 'AEAD_CHACHA20_POLY1305', family: 'ChaCha20-Poly1305',
      k: 256, n: 512, t: 128, r: 96,
      // Confidentiality uses 512-bit ChaCha20 blocks; the integrity term
      // counts Poly1305 blocks of 128 bits (L' in the draft). We expose
      // message size in bytes and convert per limit.
      blockBytes: 64, polyBlockBytes: 16
    },
    AEAD_AES_128_CCM: {
      name: 'AEAD_AES_128_CCM', family: 'CCM', k: 128, n: 128, t: 128, r: 96,
      blockBytes: 16
    },
    AEAD_AES_128_CCM_8: {
      name: 'AEAD_AES_128_CCM_8', family: 'CCM', k: 128, n: 128, t: 64, r: 96,
      blockBytes: 16
    },
    AEAD_AEGIS128L: {
      name: 'AEAD_AEGIS128L', family: 'AEGIS', k: 128, n: 128, t: 128, r: 128,
      tagBits: [128, 256], rateBytes: 32, differentialForgeryBits: 216
    },
    AEAD_AEGIS256: {
      name: 'AEAD_AEGIS256', family: 'AEGIS', k: 256, n: 128, t: 128, r: 256,
      tagBits: [128, 256], rateBytes: 16, differentialForgeryBits: 256
    }
  };

  /*
   * The AEGIS sources state security claims and attack complexities rather
   * than a reduction-style, q- and data-dependent advantage bound like the
   * bounds used elsewhere in this explorer. AEGIS v1.1 Claim 1 gives about
   * v/2^t success after v online forgery attempts for tags up to 128 bits.
   *
   * For a 256-bit tag, [SSI24] gives an exact 2^-216 differential
   * characteristic that directly yields a state-collision forgery against
   * AEGIS-128L. Repeating that chosen-message/verification trial v times gives
   * the attack-specific v/2^216 model when enough chosen-message material is
   * available. For AEGIS-256, generic 256-bit tag guessing is binding and the
   * paper supports 256-bit security against the differential attack class.
   */
  function applyAegisClaims(res, lP, lO, multiUser, selectedTagBits) {
    var alg = res.alg;
    var tagBits = selectedTagBits === 256 ? 256 : 128;
    // The cheapest modeled online forgery is either generic tag guessing or
    // the best differential forgery reported by SSI24.
    var forgeryBits = Math.min(tagBits, alg.differentialForgeryBits);
    res.ca = function () { return -Infinity; };
    res.ia = function (lv) { return lv - forgeryBits; };
    res.aea = function (lq, lv) {
      return log2sum(res.ia(lv), lO - alg.k);
    };
    res.qLimit = Infinity;
    res.vLimit = lP + forgeryBits;
    res.tagBits = tagBits;
    res.forgeryBits = forgeryBits;
    res.claimBased = true;

    if (tagBits === 128) {
      res.notes.push('AEGIS v1.1 Claim 1 says that v online forgery attempts ' +
        'succeed with probability about v/2^128. This is a design claim, not ' +
        'a reduction-style proof.');
    } else if (alg.differentialForgeryBits < tagBits) {
      res.notes.push('For AEGIS-128L, SSI24 gives a 2^-216 differential ' +
        'characteristic that directly yields a state-collision forgery. With ' +
        'one chosen-message source and one online verification per trial, the ' +
        'attack-specific model is about v/2^216 when enough source material is ' +
        'available. This is not a general reduction-style advantage proof.');
    } else {
      res.notes.push('For AEGIS-256 with a 256-bit tag, generic tag guessing ' +
        'gives about v/2^256 success, and SSI24 supports 256-bit security ' +
        'against the differential attack class it studies. This remains a ' +
        'claim-based model rather than a general reduction-style proof.');
    }
    res.notes.push('Birthday collisions among ' + tagBits + '-bit tag outputs ' +
      'become likely near 2^' + (tagBits / 2) + ' encrypted messages, but two ' +
      'already-valid tuples sharing a tag do not constitute a fresh forgery ' +
      'and do not imply an AEGIS internal-state collision. Therefore that ' +
      'output-collision probability does not add a q^2/2^' + tagBits +
      ' integrity term.');
    res.notes.push('The functional maximum for each plaintext and associated-data ' +
      'input is 2^61 - 1 bytes; the message-size control is far below that cap.');
    if (alg.r === 128) {
      res.notes.push('Nonces must not repeat. If they are chosen randomly, the ' +
        'AEGIS specification recommends at most 2^48 messages per key ' +
        '(about 2^-33 nonce-collision probability).');
    } else {
      res.notes.push('Nonces must not repeat. The AEGIS specification describes ' +
        'random 256-bit nonces as having no practical message-count limit.');
    }
    if (multiUser) {
      res.warnings.push('The AEGIS multi-key result assumes every key has a ' +
        'unique identifier encoded in unused nonce bits, which is the condition ' +
        'for the specification\'s no-multi-target-advantage statement. Without ' +
        'that domain separation, the cited sources do not provide a quantitative ' +
        'multi-user advantage bound.');
    }
    return res;
  }

  /* =====================================================================
   * Single-key setting (Section 6)
   * =====================================================================
   * params:  lP (target advantage p, log2), lL (max message length in
   *          blocks, log2), lO (offline work, log2), tagBits (AEGIS only).
   * returns: advantage functions (log2 q/v -> log2 advantage) and
   *          inverse limits (log2 count), plus assumption warnings.
   */
  function singleKey(algName, lP, lL, lO, tagBits) {
    var alg = ALGORITHMS[algName];
    var res = {
      alg: alg, setting: 'su',
      ca: null, ia: null, aea: null,   // advantage functions (log2 -> log2)
      qLimit: Infinity, vLimit: Infinity,
      warnings: [], notes: []
    };
    var fam = alg.family;
    var f;

    if (fam === 'AEGIS') {
      return applyAegisClaims(res, lP, lO, false, tagBits);
    } else if (fam === 'GCM') {
      // CA <= (s + q + 1)^2 / 2^129 with s <= q*L  (Section 6.2.1)
      res.ca = function (lq) {
        return 2 * log2sum(lq + lL, log2sum(lq, 0)) - 129;
      };
      res.qLimit = log2diff(lP / 2 + 64.5, 0) - Math.log2(Math.pow(2, lL) + 1);
      // exact form of the draft's (sqrt(p)*2^64.5 - 1)/(L+1)
      // Exact 96-bit-nonce integrity result from [GCMProofs], Equation (22):
      //
      // IA <= v*(L+1)/2^128 * delta_128(s+q+v+1), with s <= q*L.
      //
      // The draft replaces delta with 2 when s+q+v < 2^64. Evaluating delta
      // directly removes that simplifying assumption. Keep the draft's
      // independent v <= 2^64 cap.
      res.ia = function (lv, lq) {
        if (lq === undefined) lq = res.qLimit;
        var lA = log2sum(lq + lL, log2sum(lq, log2sum(lv, 0)));
        return lv + Math.log2(Math.pow(2, lL) + 1) - 128 +
          log2GcmDelta(lA, 128);
      };
      if (res.ia(64, res.qLimit) <= lP) {
        res.vLimit = 64;
      } else {
        var vlo = -1024, vhi = 64;
        for (var vi = 0; vi < 100; vi++) {
          var vmid = (vlo + vhi) / 2;
          if (res.ia(vmid, res.qLimit) <= lP) vlo = vmid;
          else vhi = vmid;
        }
        res.vLimit = vlo;
      }
      // Offline key search applies to every mode (Section 6.1).
      res.aea = function (lq, lv) {
        return log2sum(res.ca(lq), log2sum(res.ia(lv, lq), lO - alg.k));
      };
    } else if (fam === 'ChaCha20-Poly1305') {
      // CA <= 0: no limit beyond the PRF security of ChaCha20 (Section 6.3.1).
      res.ca = function () { return -Infinity; };
      res.qLimit = Infinity;
      // Corrected single-user theorem:
      //   IA <= v*epsilon(L')/2^128, epsilon(L') = 2^25*(L'+1).
      // This is algebraically equal to v*(L'+1)/2^103; evaluating epsilon
      // explicitly makes clear that 2^103 is not a rounded approximation.
      // Here lL is log2(L'), including the worst-case separate block rounding
      // of AAD and plaintext performed by compute().
      var lPolyEpsilon = 25 + Math.log2(Math.pow(2, lL) + 1);
      res.ia = function (lv) {
        return lv + lPolyEpsilon - 128;
      };
      res.vLimit = lP + 128 - lPolyEpsilon;
      res.aea = function (lq, lv) {
        return log2sum(res.ia(lv), lO - alg.k);
      };
      res.poly1305Epsilon = lPolyEpsilon;
      res.notes.push('Confidentiality: CA <= 0 — no limit on q beyond the ' +
        'PRF security of the ChaCha20 block function (and offline key search).');
    } else if (fam === 'CCM') {
      // CA <= (2Lq)^2 / 2^128  (Section 6.4.1)
      res.ca = function (lq) { return 2 * (1 + lL + lq) - 128; };
      // q <= sqrt(p)*2^64 / (2L) = sqrt(p)*2^63/L
      var qFromCA = lP / 2 + 63 - lL;
      // IA <= v/2^t + (2L(v+q))^2 / 2^128  (Section 6.4.2)
      res.ia = function (lv, lq) {
        return log2sum(lv - alg.t, 2 * (1 + lL + log2sum(lv, lq)) - 128);
      };
      res.aea = function (lq, lv) {
        return log2sum(res.ca(lq), log2sum(res.ia(lv, lq), lO - alg.k));
      };
      if (alg.t === 128) {
        // Draft simplification (v negligible next to the quadratic term):
        // v + q <= sqrt(p) * 2^63 / L.
        res.vLimit = lP / 2 + 63 - lL;
        res.qLimit = minLog2(qFromCA, res.vLimit);
        res.notes.push('Integrity limit uses the draft simplification ' +
          'v + q <= sqrt(p)·2^63/L, so q and v share one combined budget.');
      } else {
        // CCM_8: split the target evenly — v*2^64 <= p*2^127 gives
        // v <= p*2^63, and (2L(v+q))^2 <= p*2^127 gives the v+q budget
        // (Section 6.5).
        res.vLimit = lP + 63;
        var vq = (lP - 1) / 2 + 63 - lL;
        res.qLimit = minLog2(qFromCA, log2diff(vq, res.vLimit));
        res.notes.push('Short 64-bit tag: the draft splits the target as ' +
          'v·2^64 <= p·2^127 and (2L(v+q))^2 <= p·2^127, which makes v the ' +
          'binding constraint and allows a slightly larger q.');
      }
    }
    return res;
  }

  /* =====================================================================
   * Multi-key setting (Section 7)
   * =====================================================================
   * params:  lP, lL, lO and tagBits as above; lB = log2(max blocks encrypted
   *          by any key), nonceMode = 'randomized' (TLS 1.3 / QUIC style
   *          nonce randomization) or 'implicit' (random partially implicit
   *          nonces, TLS 1.2 style — GCM only).
   */
  function multiKey(algName, lP, lL, lO, lB, nonceMode, tagBits) {
    var alg = ALGORITHMS[algName];
    var res = {
      alg: alg, setting: 'mu', nonceMode: nonceMode,
      ca: null, ia: null, aea: null,
      qLimit: Infinity, vLimit: Infinity,
      warnings: [], notes: []
    };
    var fam = alg.family;

    if (fam === 'AEGIS') {
      return applyAegisClaims(res, lP, lO, true, tagBits);
    } else if (fam === 'GCM') {
      if (nonceMode === 'implicit') {
        // Random, partially implicit nonces (Theorem 5.3 of [GCM-MU2]).
        // AEA <= ((q+v)*o + (q+v)^2)/2^(k+26) + (q+v)*L*B/2^127
        var gcmAeTerm = function (lx) {
          return log2sum(log2sum(lx + lO, 2 * lx) - (alg.k + 26),
                         lx + lL + lB - 127);
        };
        res.aea = function (lq, lv) { return gcmAeTerm(log2sum(lq, lv)); };
        res.ca = function (lq) {
          return log2sum(log2sum(lq + lO, 2 * lq) - (alg.k + 26),
                         lq + lL + lB - 127);
        };
        res.ia = res.aea; // IA <= AEA (Section 7.1.3)
        if (alg.k === 256) {
          // First term negligible: q+v <= p*2^127/(L*B).
          // The draft's examples assume equal proportions for q and v,
          // so each individually gets half of the combined budget.
          res.qLimit = lP + 126 - lL - lB;
          res.vLimit = res.qLimit;
          res.notes.push('With k = 256 the offline-work term of the TLS 1.2 ' +
            'style bound is negligible, so the limit matches nonce randomization.');
        } else {
          // k = 128, assuming o <= q+v:
          // q+v <= min(sqrt(p)*2^76, p*2^126/(L*B)); halved for q and v
          // individually under the equal-proportions assumption.
          res.qLimit = minLog2(lP / 2 + 75, lP + 125 - lL - lB);
          res.vLimit = res.qLimit;
          res.notes.push('k = 128 with o <= q+v assumed: ' +
            'q+v <= min(√p·2^76, p·2^126/(L·B)).');
        }
      } else {
        // Nonce randomization: AEA <= (q+v)*L*B/2^127  (Section 7.1.1)
        res.aea = function (lq, lv) {
          return log2sum(lq, lv) + lL + lB - 127;
        };
        res.ca = function (lq) { return lq + lL + lB - 127; };
        res.ia = res.aea;
        // The draft's example table (Table 3) assumes equal proportions for
        // q and v, i.e. each gets half of the combined q+v budget:
        // q, v <= p*2^126/(L*B), written there as 2^69/B for p = 2^-50.
        res.qLimit = lP + 126 - lL - lB;
        res.vLimit = res.qLimit;
        res.notes.push('q and v share the combined budget ' +
          'q+v <= p·2^127/(L·B); shown per-value assuming equal proportions, ' +
          'as in the draft example table.');
        if (lB < Math.log2(100)) {
          res.warnings.push('Assumption B ≫ 100 is not met (B ≈ 2^' +
            lB.toFixed(1) + '). The draft says B should be increased by ' +
            (alg.k === 128 ? '161' : '97') + ' in this regime; the limits ' +
            'shown are optimistic. (Section 7.1.1)');
        }
        if (alg.k === 128 && lO > 70) {
          res.warnings.push('For AEAD_AES_128_GCM the bound assumes ' +
            'o <= 2^70; above that a term of order o/2^120 starts ' +
            'dominating and is not modelled here. (Section 7.1.1)');
        }
      }
    } else if (fam === 'ChaCha20-Poly1305') {
      // Exact nonce-randomized multi-user bound from [ChaCha20Poly1305-MU],
      // Theorem 7.2, instantiated with n=512, k=256, t=128 and mu=96.
      // The draft retains only the dominant v*epsilon(L')/2^128 term.
      var lMultiPolyEpsilon = 25 + Math.log2(Math.pow(2, lL) + 1);
      var lMultiPolyEpsilonPlus3 = log2sum(lMultiPolyEpsilon, Math.log2(3));
      // All allowed bytes may be plaintext, so this is the maximum number of
      // encrypted 512-bit ChaCha20 blocks in one query.
      var messageBlocks = Math.max(1,
        Math.ceil((Math.pow(2, lL) - 1) / 4));
      var lMessageBlocks = Math.log2(messageBlocks);

      function exactChaChaMultiAea(lq, lv) {
        var bound = log2sum(
          lv + lMultiPolyEpsilonPlus3 - 128,
          log2sum(
            1 + lO + Math.log2(512 - 256) - 256,
            log2sum(
              1 + lv + Math.log2(512 - 256 + 4 * 128) - 256,
              log2sum(-254, -254))));

        if (lq === -Infinity) return bound;

        var lSigma = lq + lMessageBlocks;
        bound = log2sum(bound, 2 * log2sum(lSigma, lq) - 513);

        // The theorem permits any delta > 0. For each integer d, the largest
        // delta that leaves d unchanged minimizes the nonce-randomization
        // term. The remaining discrete objective
        //
        //   d*(o+q)/2^256 + 2^(-delta*96)
        //
        // is convex. Evaluate the integers around its stationary point and
        // the smallest d satisfying Theorem 7.2's q-range condition.
        var denominator = Math.max(1, 96 - lq);
        var dScale = 96 / denominator;
        var deltaRequired = lq > 100 ? Math.pow(2, lq - 100) - 1 : 0;
        var dRequired = deltaRequired > 0
          ? Math.ceil(dScale * (deltaRequired + 1)) - 1
          : Math.floor(dScale);
        var lWorkAndQueries = log2sum(lO, lq);
        var lCoefficient = lWorkAndQueries - 256;
        var stationary = (96 - lCoefficient +
          Math.log2(Math.LN2 * denominator)) / denominator - 1;
        var center = Math.max(dRequired, stationary);
        var candidates = [
          dRequired,
          Math.max(dRequired, Math.floor(center) - 1),
          Math.max(dRequired, Math.floor(center)),
          Math.max(dRequired, Math.ceil(center)),
          Math.max(dRequired, Math.ceil(center) + 1)
        ];
        var bestVariable = Infinity;
        for (var ci = 0; ci < candidates.length; ci++) {
          var d = candidates[ci];
          var delta = ((d + 1) * denominator / 96) - 1;
          if (delta <= 0 ||
              lq > 96 + Math.log2((delta + 1) * 96 / 6) + 1e-12) {
            continue;
          }
          var dTerm = d === 0
            ? -Infinity
            : Math.log2(d) + lCoefficient;
          var variable = log2sum(dTerm, -delta * 96);
          if (variable < bestVariable) bestVariable = variable;
        }
        return log2sum(bound, bestVariable);
      }

      function solveExactLimit(fn, cap) {
        if (fn(-Infinity) > lP) return -Infinity;
        if (fn(cap) <= lP) return cap;
        var lo = -1024, hi = cap;
        for (var i = 0; i < 100; i++) {
          var mid = (lo + hi) / 2;
          if (fn(mid) <= lP) lo = mid;
          else hi = mid;
        }
        return lo;
      }

      res.aea = exactChaChaMultiAea;
      res.ia = function (lv, lq) {
        return exactChaChaMultiAea(
          lq === undefined ? -Infinity : lq, lv);
      };
      res.ca = function (lq) {
        return exactChaChaMultiAea(lq, -Infinity);
      };
      var lSigmaCap = 256 + Math.log2(256 / 6);
      var lQTheoremCap = lSigmaCap - lMessageBlocks;
      res.qLimit = solveExactLimit(res.ca, lQTheoremCap);
      res.vLimit = solveExactLimit(function (lv) {
        return res.ia(lv);
      }, 510);
      if (res.qLimit === lQTheoremCap) {
        res.qLimitReason = 'theorem encrypted-block cap';
      }
      res.poly1305Epsilon = lMultiPolyEpsilon;
      res.chachaMessageBlocks = messageBlocks;
      res.notes.push('Uses every term of the nonce-randomized multi-user ' +
        'bound in Theorem 7.2 and minimizes its delta/d tradeoff. q and v ' +
        'are totals across all keys; L\' is the maximum per query.');
      if (nonceMode === 'implicit') {
        res.warnings.push('The exact multi-key calculation shown is for the ' +
          '96-bit XN nonce-randomization transform, not the TLS 1.2-style ' +
          'partially implicit nonce construction selected here.');
      }
    } else if (fam === 'CCM') {
      // AEA <= (q+v)*L*B/2^127 + v/2^t + o/2^(k-6); assuming o <= q+v the
      // draft splits the target evenly across the first two terms
      // (Section 7.3). C = max blocks encrypted or decrypted by any key.
      res.aea = function (lq, lv) {
        return log2sum(log2sum(lq, lv) + lL + lB - 127,
                       log2sum(lv - alg.t, lO - (alg.k - 6)));
      };
      res.ca = function (lq) { return lq + lL + lB - 127; };
      res.ia = function (lv) { return lv - alg.t; };
      res.qLimit = lP - 1 + 127 - lL - lB; // even split: first term gets p/2
      res.vLimit = minLog2(lP - 1 + alg.t, res.qLimit);
      res.notes.push('Assumes o <= q+v and splits the target evenly between ' +
        'the (q+v)L·C/2^127 and v/2^t terms. The UI field is C (blocks ' +
        'encrypted or decrypted per key).');
      if (alg.t === 64) {
        res.notes.push('With the 64-bit tag, v <= p·2^(t-1) is usually the ' +
          'binding constraint.');
      }
    }
    return res;
  }

  /*
   * Compose a nonce-respecting result with the probability that independently
   * sampled nonces repeat under one key.  The existing q/v limits are
   * evaluated at half of the requested advantage and the other half is
   * reserved for the collision event.  Each affected CA, IA, or AEA expression
   * is composed from those two terms.  This does not change the underlying
   * result's conventions for allocating simultaneous q and v.
   *
   * The caller has already evaluated the conditional result at lP - 1 when a
   * collision is possible.  lM is log2(max protected messages per key) in the
   * multi-key setting and Infinity in the single-key setting.
   */
  function applyRandomNonces(res, lP, lM) {
    var alg = res.alg;
    var multiUser = res.setting === 'mu';
    var maxPerKey = multiUser
      ? (lM === undefined ? Infinity : Math.max(0, lM))
      : Infinity;
    var collisionPossible = maxPerKey !== 0;
    var collisionTarget = collisionPossible ? lP - 1 : -Infinity;
    var baseCA = res.ca;
    var baseIA = res.ia;
    var baseAEA = res.aea;

    res.nonceMode = 'random';
    res.randomNonces = true;
    res.randomNonceBits = alg.r;
    res.lM = maxPerKey;
    res.conditionalQLimit = res.qLimit;
    res.conditionalVLimit = res.vLimit;
    res.collisionBudget = collisionTarget;
    res.nonceCollision = function (lq) {
      return randomNonceCollision(lq, alg.r, maxPerKey);
    };
    res.nonceRespectingCa = baseCA;
    res.nonceRespectingIa = baseIA;
    res.nonceRespectingAea = baseAEA;

    if (baseCA) {
      res.ca = function (lq) {
        return log2sum(baseCA(lq), res.nonceCollision(lq));
      };
    }
    if (baseIA) {
      res.ia = function (lv, lq) {
        var conditional = baseIA.length >= 2 ? baseIA(lv, lq) : baseIA(lv);
        var collision = lq === undefined ? -Infinity : res.nonceCollision(lq);
        return log2sum(conditional, collision);
      };
    }
    if (baseAEA) {
      res.aea = function (lq, lv) {
        return log2sum(baseAEA(lq, lv), res.nonceCollision(lq));
      };
    }

    res.collisionLimit = collisionPossible
      ? randomNonceLimit(collisionTarget, alg.r, maxPerKey)
      : Infinity;
    if (res.collisionLimit < res.qLimit) {
      res.qLimit = res.collisionLimit;
      res.qLimitReason = 'nonce collision';
    }

    if (collisionPossible) {
      res.notes.push('Random-nonce mode evaluates the nonce-respecting q/v ' +
        'limits at half of the target advantage and reserves the other half ' +
        'for same-key nonce collisions. The collision probability is bounded by ' +
        (multiUser
          ? 'q·(min(q,M)-1)/2^(' + (alg.r + 1) + '), where M is the maximum messages per key.'
          : 'q·(q-1)/2^' + (alg.r + 1) + '.')
      );
    } else {
      res.notes.push('With at most one protected message per key, independently ' +
        'sampled nonces cannot collide under the same key, so no collision ' +
        'budget is needed.');
    }
    res.notes.push('The random-nonce calculation assumes independent, uniform, ' +
      'full-width samples from a cryptographically secure random generator ' +
      'and treats any same-key repeat as a security failure.');
    if (multiUser) {
      res.warnings.push('The AEAD-limits draft gives the concrete multi-key ' +
        'bounds used here for nonce randomization, not for independently ' +
        'sampled full nonces. This view uses that result as a conditional ' +
        'model and adds the same-key collision event; it is not a dedicated ' +
        'random-nonce multi-key proof.');
    }

    // Algorithm specifications impose requirements beyond the probability
    // calculation.  Keep those visible and, where a per-key numerical cap is
    // stated, enforce it for a single key and validate M for multiple keys.
    var perKeyCap;
    if (alg.family === 'GCM') {
      perKeyCap = 32;
      res.notes.push('NIST SP 800-38D permits its RBG-based IV construction ' +
        'but limits authenticated-encryption invocations to 2^32 per key ' +
        'across supported IV lengths.');
    } else if (alg.family === 'ChaCha20-Poly1305') {
      res.notes.push('RFC 8439 specifies unique, non-random nonces for its ' +
        'ChaCha20-Poly1305 profile. This independent-random-nonce view is a ' +
        'probabilistic construction outside that profile: it conditions the ' +
        'AEAD bound on no repeat and adds the same-key collision probability.');
    } else if (alg.family === 'CCM') {
      res.warnings.push('CCM requires a unique nonce for every invocation ' +
        'with a key. Independent random sampling cannot guarantee uniqueness; ' +
        'the numerical result illustrates collision risk rather than a ' +
        'conforming CCM nonce construction.');
    } else if (alg.name === 'AEAD_AEGIS128L') {
      perKeyCap = 48;
    }

    if (perKeyCap !== undefined) {
      res.randomNoncePerKeyCap = perKeyCap;
      if (!multiUser && perKeyCap < res.qLimit) {
        res.qLimit = perKeyCap;
        res.qLimitReason = alg.family === 'GCM'
          ? 'random-nonce invocation cap'
          : 'random-nonce guidance';
      } else if (multiUser && maxPerKey > perKeyCap) {
        res.warnings.push('M exceeds the ' + alg.name + ' per-key random-nonce ' +
          (alg.family === 'GCM' ? 'invocation cap' : 'guidance') +
          ' of 2^' + perKeyCap + '. Reduce M or rekey earlier.');
      }
    }

    // Make the displayed integrity limit usable together with the displayed
    // confidentiality limit: if the underlying convention leaves IA slightly
    // above p at (qLimit, vLimit), tighten v monotonically until the composed
    // IA reaches p.  This matters in particular for CCM, whose integrity term
    // contains q and v together.
    if (res.ia && isFinite(res.vLimit) &&
        res.ia(res.vLimit, res.qLimit) > lP) {
      var lo = -1024;
      var hi = res.vLimit;
      if (res.ia(-Infinity, res.qLimit) > lP) {
        res.vLimit = -Infinity;
      } else {
        for (var i = 0; i < 100; i++) {
          var mid = (lo + hi) / 2;
          if (res.ia(mid, res.qLimit) <= lP) lo = mid;
          else hi = mid;
        }
        res.vLimit = lo;
      }
      res.vLimitReason = 'at the displayed q limit';
    }
    return res;
  }

  /* ---------- orchestration ---------- */

  // opts: { alg, setting, nonceMode, tagBits, lP, lLBytes, lO, lB, lM }
  // lLBytes: log2 of max message size in BYTES (plaintext + AAD).
  // lB: log2 of max blocks (128-bit) encrypted (B) or encrypted+decrypted
  //     (C) per key, multi-key only.
  // lM: log2 of max protected messages per key, used for independently random
  //     nonces in the multi-key setting. If omitted, all q messages are
  //     conservatively allowed to fall under one key.
  function compute(opts) {
    var alg = ALGORITHMS[opts.alg];
    // Convert the byte length into the block count used by each bound.
    // GCM/CCM use 128-bit blocks. For ChaCha20-Poly1305, L' is
    // ceil(|AAD|/16) + ceil(|plaintext|/16); because the UI bounds their sum,
    // use the largest L' possible over every split of that byte allowance.
    // The claim-based AEGIS model does not depend on lL.
    var lL = opts.lLBytes - 4; // log2(bytes/16)
    if (lL < 0) lL = 0;
    var poly1305Blocks;
    if (alg.family === 'ChaCha20-Poly1305') {
      poly1305Blocks = maxPoly1305DataBlocks(opts.lLBytes);
      lL = Math.log2(poly1305Blocks);
    }
    var nonceMode = opts.nonceMode || 'randomized';
    var lM = opts.lM === undefined ? Infinity : Math.max(0, opts.lM);
    var collisionPossible = nonceMode === 'random' &&
      (opts.setting !== 'mu' || lM !== 0);
    var conditionalTarget = collisionPossible ? opts.lP - 1 : opts.lP;
    // The published multi-key formulas use nonce randomization. For the
    // independently random option we expose their use as a conditional model,
    // then add the collision bad event and warn that this is not a dedicated
    // random-nonce multi-key proof.
    var conditionalNonceMode = nonceMode === 'implicit' ? 'implicit' : 'randomized';
    var r = opts.setting === 'mu'
      ? multiKey(opts.alg, conditionalTarget, lL, opts.lO, opts.lB,
                 conditionalNonceMode, opts.tagBits)
      : singleKey(opts.alg, conditionalTarget, lL, opts.lO, opts.tagBits);
    r.lL = lL;
    if (poly1305Blocks !== undefined) r.poly1305Blocks = poly1305Blocks;
    r.targetAdvantage = opts.lP;
    r.conditionalTarget = conditionalTarget;
    if (nonceMode === 'random') applyRandomNonces(r, opts.lP, lM);
    else r.nonceMode = nonceMode;
    return r;
  }

  var API = {
    ALGORITHMS: ALGORITHMS,
    compute: compute,
    singleKey: singleKey,
    multiKey: multiKey,
    log2sum: log2sum,
    log2diff: log2diff,
    minLog2: minLog2,
    solveQuadUsage: solveQuadUsage,
    log2PowMinusOne: log2PowMinusOne,
    log2GcmDelta: log2GcmDelta,
    maxPoly1305DataBlocks: maxPoly1305DataBlocks,
    randomNonceCollision: randomNonceCollision,
    randomNonceLimit: randomNonceLimit,
    fmtPow: fmtPow,
    fmtNum: fmtNum,
    fmtShort: fmtShort,
    LOG2_10: LOG2_10
  };

  if (typeof module !== 'undefined' && module.exports) module.exports = API;
  else global.AEADLimits = API;
})(typeof window !== 'undefined' ? window : globalThis);