aead-usage-limits / index.html
jedisct1's picture
Use exact AES-GCM bounds and model random ChaCha20-Poly1305 nonces
949aa2a verified
Raw
History Blame Contribute Delete
44.9 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>AEAD Usage Limits — interactive explorer</title>
<style>
:root {
--bg: #0f141a;
--panel: #182029;
--panel2: #1e2833;
--border: #2c3947;
--text: #e6edf3;
--muted: #93a4b5;
--accent: #4da3ff;
--accent2: #37d0a0;
--warn: #ffb454;
--bad: #ff6b6b;
--good: #37d0a0;
--mono: "SF Mono", ui-monospace, Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.5;
}
header {
padding: 20px 24px 12px;
border-bottom: 1px solid var(--border);
}
header h1 { margin: 0 0 4px; font-size: 20px; }
header p { margin: 0; color: var(--muted); font-size: 13px; max-width: 1000px; }
header a { color: var(--accent); }
main { display: grid; grid-template-columns: 320px 1fr; gap: 18px; padding: 18px 24px 40px; align-items: start; }
@media (max-width: 900px) { main { grid-template-columns: 1fr; } }
.panel { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 16px; }
.panel h2 { margin: 0 0 12px; font-size: 13px; text-transform: uppercase; letter-spacing: .06em; color: var(--muted); }
fieldset { border: 1px solid var(--border); border-radius: 8px; margin: 0 0 14px; padding: 10px 12px 12px; }
legend { font-size: 12px; color: var(--muted); padding: 0 4px; }
label.field { display: block; margin-bottom: 12px; font-size: 13px; }
label.field .val { float: right; font-family: var(--mono); color: var(--accent2); font-size: 12px; }
select, input[type=number] {
width: 100%; margin-top: 4px; background: var(--panel2); color: var(--text);
border: 1px solid var(--border); border-radius: 6px; padding: 6px 8px; font-size: 13px;
}
input[type=range] { width: 100%; margin-top: 6px; accent-color: var(--accent); }
.seg { display: flex; gap: 6px; margin-bottom: 12px; }
.seg button {
flex: 1; background: var(--panel2); color: var(--muted); border: 1px solid var(--border);
border-radius: 6px; padding: 7px 4px; font-size: 12.5px; cursor: pointer;
}
.seg button.active { background: var(--accent); color: #06233f; border-color: var(--accent); font-weight: 600; }
.hint { font-size: 11.5px; color: var(--muted); margin-top: -6px; margin-bottom: 12px; }
.presets { display: flex; flex-wrap: wrap; gap: 6px; }
.presets button {
background: var(--panel2); color: var(--text); border: 1px solid var(--border);
border-radius: 20px; padding: 5px 11px; font-size: 12px; cursor: pointer;
}
.presets button:hover { border-color: var(--accent); }
.cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(220px, 1fr)); gap: 14px; margin-bottom: 18px; }
.card { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; }
.card .k { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
.card .big { font-family: var(--mono); font-size: 26px; margin: 6px 0 2px; }
.card .sub { font-family: var(--mono); font-size: 12px; color: var(--muted); }
.card.cl .big { color: var(--accent); }
.card.il .big { color: var(--accent2); }
.card.off .big { color: var(--warn); }
.recommendation {
background: linear-gradient(135deg, rgba(55,208,160,.12), rgba(77,163,255,.08));
border: 1px solid rgba(55,208,160,.55);
border-radius: 10px;
padding: 15px 17px;
margin-bottom: 18px;
}
.recommendation .k {
color: var(--accent2);
font-size: 12px;
font-weight: 700;
letter-spacing: .06em;
text-transform: uppercase;
}
.recommendation .main { margin: 5px 0 3px; font-size: 16px; line-height: 1.45; }
.notice { border-radius: 8px; padding: 10px 12px; font-size: 12.5px; margin-bottom: 12px; }
.notice.warn { background: rgba(255,180,84,.1); border: 1px solid rgba(255,180,84,.4); color: #ffd9a0; }
.notice.info { background: rgba(77,163,255,.08); border: 1px solid rgba(77,163,255,.35); color: #bcdcff; }
.notice ul { margin: 4px 0 0; padding-left: 18px; }
table.cmp { width: 100%; border-collapse: collapse; font-size: 13px; }
table.cmp th, table.cmp td { padding: 8px 10px; text-align: right; border-bottom: 1px solid var(--border); }
table.cmp th:first-child, table.cmp td:first-child { text-align: left; }
table.cmp th { color: var(--muted); font-weight: 600; font-size: 12px; }
table.cmp td.num { font-family: var(--mono); }
table.cmp tr.sel { background: rgba(77,163,255,.08); }
table.cmp tr:hover { background: rgba(255,255,255,.03); cursor: pointer; }
.chartwrap { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 14px 16px; margin-bottom: 18px; }
.chartwrap h2 { margin: 0 0 4px; font-size: 14px; }
.chartwrap .desc { font-size: 12px; color: var(--muted); margin-bottom: 10px; }
svg { display: block; width: 100%; height: auto; }
.legend { display: flex; gap: 16px; flex-wrap: wrap; font-size: 12px; margin-top: 8px; }
.legend span { display: inline-flex; align-items: center; gap: 6px; color: var(--muted); }
.legend i { width: 14px; height: 3px; border-radius: 2px; display: inline-block; }
details { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; padding: 12px 16px; }
details summary { cursor: pointer; font-size: 13px; color: var(--accent); font-weight: 600; }
details .body { margin-top: 10px; font-size: 13px; color: var(--text); }
details code, .formula { font-family: var(--mono); font-size: 12px; background: var(--panel2); padding: 2px 5px; border-radius: 4px; }
details a { color: var(--accent); }
.fblock { font-family: var(--mono); font-size: 12.5px; background: var(--panel2); border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; margin: 8px 0; white-space: pre; overflow-x: auto; }
.muted { color: var(--muted); }
footer { padding: 0 24px 30px; color: var(--muted); font-size: 12px; }
</style>
</head>
<body>
<header>
<h1>AEAD Usage Limits — interactive explorer</h1>
<p>
Explore the confidentiality, integrity and authenticated-encryption usage
limits from
<a href="https://datatracker.ietf.org/doc/draft-irtf-cfrg-aead-limits/">draft-irtf-cfrg-aead-limits</a>.
Pick an algorithm and parameters; the limits, per-algorithm comparison and
the advantage curve update live. All math is done in the log<sub>2</sub>
domain so limits like 2<sup>64.5</sup> stay exact.
</p>
</header>
<main>
<!-- ============ controls ============ -->
<section class="panel" aria-label="controls">
<h2>Parameters</h2>
<label class="field">Algorithm
<select id="alg"></select>
</label>
<div id="tagControls" style="display:none">
<div class="field">Authentication tag</div>
<div class="seg" id="tagSeg">
<button data-v="128" class="active">128-bit</button>
<button data-v="256">256-bit</button>
</div>
<div class="hint" id="tagHint"></div>
</div>
<div class="field">Setting</div>
<div class="seg" id="settingSeg">
<button data-v="su" class="active">Single-key</button>
<button data-v="mu">Multi-key</button>
</div>
<div class="hint" id="settingHint"></div>
<div class="field" id="nonceWrap">Nonce construction</div>
<div class="seg" id="nonceSeg">
<button data-v="randomized" id="nonceRandomized" class="active">Non-repeating</button>
<button data-v="random">Random nonces</button>
<button data-v="implicit" id="nonceImplicit">Partially implicit</button>
</div>
<div class="hint" id="nonceHint"></div>
<fieldset>
<legend>Security target &amp; usage</legend>
<label class="field">Target advantage p <span class="val" id="pVal"></span>
<input type="range" id="p" min="-128" max="-10" step="1" value="-50">
</label>
<div class="hint">Upper bound on attacker success probability (CA, IA or AEA).</div>
<label class="field">Max message size <span class="val" id="lVal"></span>
<input type="range" id="lsize" min="4" max="20" step="1" value="11">
</label>
<div class="hint">Largest message (plaintext + AAD), in bytes. Converted to L for bounds that depend on message length; the cited AEGIS claims do not.</div>
<label class="field">Offline work o <span class="val" id="oVal"></span>
<input type="range" id="owork" min="0" max="128" step="1" value="70">
</label>
<div class="hint">Attacker's precomputation, in cipher operations.</div>
</fieldset>
<fieldset id="muParams" style="display:none">
<legend>Multi-key</legend>
<label class="field"><span id="bcLabel">B — max blocks / key</span> <span class="val" id="bVal"></span>
<input type="range" id="bslide" min="0" max="64" step="1" value="27">
</label>
<div class="hint" id="bcHint"></div>
<div id="mWrap" style="display:none">
<label class="field">M — max messages / key <span class="val" id="mVal"></span>
<input type="range" id="mslide" min="0" max="64" step="1" value="20">
</label>
<div class="hint">Needed to aggregate same-key random-nonce collision risk; independent of B/C.</div>
</div>
</fieldset>
<fieldset>
<legend>Presets</legend>
<div class="presets" id="presets"></div>
</fieldset>
</section>
<!-- ============ results ============ -->
<section>
<div class="recommendation" aria-live="polite">
<div class="k">Recommendation</div>
<p class="main" id="recommendationText"></p>
</div>
<div id="warnings"></div>
<div class="cards">
<div class="card cl">
<div class="k">Confidentiality limit — max q</div>
<div class="big" id="qBig"></div>
<div class="sub" id="qSub">protected messages</div>
</div>
<div class="card il">
<div class="k">Integrity limit — max v</div>
<div class="big" id="vBig"></div>
<div class="sub" id="vSub">forgery attempts</div>
</div>
<div class="card off">
<div class="k">Offline-work floor (o/2^k)</div>
<div class="big" id="offBig"></div>
<div class="sub">advantage regardless of usage</div>
</div>
</div>
<div class="chartwrap">
<h2>Advantage vs. usage</h2>
<div class="desc" id="chartDesc"></div>
<svg id="chart" viewBox="0 0 720 340" role="img" aria-label="advantage curve"></svg>
<div class="legend" id="chartLegend"></div>
</div>
<div class="panel" style="margin-bottom:18px">
<h2>All algorithm groups at these parameters</h2>
<table class="cmp" id="cmpTable">
<thead>
<tr><th>AEAD / equivalent-limit group</th><th>max q</th><th>max v</th><th>binding</th></tr>
</thead>
<tbody></tbody>
</table>
</div>
<details>
<summary>Formulas &amp; assumptions for <span id="fAlg"></span> (<span id="fSetting"></span>)</summary>
<div class="body" id="formulaBody"></div>
</details>
</section>
</main>
<footer>
Limits are illustrative results from the cited analyses and security claims,
computed under their simplifying assumptions (including the selected nonce
construction and a single message size covering AAD + plaintext). Random-nonce
results treat any same-key nonce repeat as a security failure. A claim-based
result is not a provable advantage bound. Always validate assumptions for your
deployment. Not security advice.
</footer>
<script src="js/aead.js"></script>
<script>
(function () {
'use strict';
var A = window.AEADLimits;
var $ = function (id) { return document.getElementById(id); };
var DEFAULT_STATE = {
alg: 'AEAD_AES_128_GCM',
setting: 'su',
nonceMode: 'randomized',
tagBits: 128,
lP: -50,
lLBytes: 11,
lO: 70,
lB: 27,
lM: 20
};
var state = Object.assign({}, DEFAULT_STATE);
var ALG_NAMES = Object.keys(A.ALGORITHMS);
var AEGIS_NAMES = ['AEAD_AEGIS128L', 'AEAD_AEGIS256'];
/* ---------- build controls ---------- */
var algSel = $('alg');
function addAlgorithmGroup(label, names) {
var group = document.createElement('optgroup');
group.label = label;
names.forEach(function (n) {
var o = document.createElement('option');
o.value = n; o.textContent = n;
group.appendChild(o);
});
algSel.appendChild(group);
}
addAlgorithmGroup('AEAD limits draft', ALG_NAMES.filter(function (n) {
return A.ALGORITHMS[n].family !== 'AEGIS';
}));
addAlgorithmGroup('AEGIS', AEGIS_NAMES);
algSel.value = state.alg;
var COMPARISON_ROWS = ALG_NAMES.map(function (name) {
var label = name;
if (name === 'AEAD_AEGIS128L') label = 'AEGIS-128L/X2/X4';
if (name === 'AEAD_AEGIS256') label = 'AEGIS-256/X2/X4';
return { name: name, label: label };
});
function preset(label, overrides) {
return { label: label, s: Object.assign({}, DEFAULT_STATE, overrides) };
}
var PRESETS = [
preset('Draft example (SU, AES-128-GCM)', {}),
preset('Draft example (MU, AES-128-GCM)', { setting: 'mu' }),
preset('Cautious (AES-256-GCM, 2^-80)', {
alg: 'AEAD_AES_256_GCM', setting: 'mu', lP: -80, lO: 80, lB: 32
}),
preset('TLS 1.2-style nonces (AES-128-GCM)', {
setting: 'mu', nonceMode: 'implicit', lO: 32
}),
preset('Random nonces (AEGIS-128L)', {
alg: 'AEAD_AEGIS128L', nonceMode: 'random'
})
];
var presetsEl = $('presets');
PRESETS.forEach(function (p) {
var b = document.createElement('button');
b.textContent = p.label;
b.addEventListener('click', function () {
state = Object.assign({}, p.s);
syncControls(); render();
});
presetsEl.appendChild(b);
});
/* ---------- helpers ---------- */
function isGCM() { return A.ALGORITHMS[state.alg].family === 'GCM'; }
function isCCM() { return A.ALGORITHMS[state.alg].family === 'CCM'; }
function isAEGIS() { return A.ALGORITHMS[state.alg].family === 'AEGIS'; }
function params() {
return {
alg: state.alg, setting: state.setting, nonceMode: state.nonceMode,
tagBits: state.tagBits, lP: state.lP, lLBytes: state.lLBytes,
lO: state.lO, lB: state.lB, lM: state.lM
};
}
/* ---------- control syncing ---------- */
function syncControls() {
algSel.value = state.alg;
$('p').value = state.lP;
$('lsize').value = state.lLBytes;
$('owork').value = state.lO;
$('bslide').value = state.lB;
$('mslide').value = state.lM;
var mu = state.setting === 'mu';
if (state.nonceMode === 'implicit' && !(mu && isGCM())) {
state.nonceMode = 'randomized';
}
segSet('settingSeg', state.setting);
segSet('nonceSeg', state.nonceMode);
segSet('tagSeg', state.tagBits);
$('muParams').style.display = mu ? '' : 'none';
$('mWrap').style.display = mu && state.nonceMode === 'random' ? '' : 'none';
$('nonceImplicit').style.display = mu && isGCM() ? '' : 'none';
$('nonceRandomized').textContent = mu && !isAEGIS()
? 'Nonce randomization'
: 'Non-repeating';
$('tagControls').style.display = isAEGIS() ? '' : 'none';
$('tagHint').textContent = state.tagBits === 256
? 'AEGIS-128L: 216-bit differential-forgery model. AEGIS-256: 256-bit tag-guessing and differential model.'
: 'AEGIS v1.1 claims about v/2^128 success after v online forgery attempts.';
$('settingHint').textContent = mu
? 'Limits are totals across all keys (e.g. rekeying, many connections).'
: 'One key shared between two parties.';
if (state.nonceMode === 'random') {
$('nonceHint').textContent = 'Independent uniform full-width samples; same-key collision risk is included in the limits.';
} else if (state.nonceMode === 'implicit') {
$('nonceHint').textContent = 'TLS 1.2 style: random implicit part + non-repeating explicit part.';
} else if (mu && !isAEGIS()) {
$('nonceHint').textContent = 'TLS 1.3 / QUIC style: a non-repeating nonce is XORed with a secret random offset.';
} else if (mu) {
$('nonceHint').textContent = 'Non-repeating nonce; the AEGIS multi-key model assumes a unique per-key identifier in unused nonce bits.';
} else {
$('nonceHint').textContent = 'A counter, permutation, or other construction that guarantees no repeats under one key.';
}
var isC = isCCM(), isA = isAEGIS();
var isChacha = A.ALGORITHMS[state.alg].family === 'ChaCha20-Poly1305';
$('bcLabel').textContent = (isA || isChacha) ? 'B/C — per-key blocks (other AEADs)'
: (isC ? 'C — max blocks enc+dec / key' : 'B — max blocks enc / key');
$('bcHint').textContent = (isA || isChacha)
? 'Not used by the selected algorithm; retained for the other rows in the comparison table.'
: (isC
? 'Largest number of 128-bit blocks encrypted or decrypted by any single key.'
: 'Largest number of 128-bit blocks encrypted by any single key.');
}
function segSet(id, v) {
var el = $(id);
Array.prototype.forEach.call(el.children, function (b) {
b.classList.toggle('active', b.getAttribute('data-v') === String(v));
});
}
function segWire(id, key) {
$(id).addEventListener('click', function (e) {
var b = e.target.closest('button');
if (!b) return;
state[key] = b.getAttribute('data-v');
syncControls(); render();
});
}
segWire('settingSeg', 'setting');
segWire('nonceSeg', 'nonceMode');
$('tagSeg').addEventListener('click', function (e) {
var b = e.target.closest('button');
if (!b) return;
state.tagBits = parseInt(b.getAttribute('data-v'), 10);
syncControls(); render();
});
function selectAlgorithm(name) {
state.alg = name;
if (A.ALGORITHMS[name].family !== 'AEGIS') state.tagBits = 128;
syncControls(); render();
}
algSel.addEventListener('change', function () { selectAlgorithm(algSel.value); });
function slider(id, key, fmt) {
$(id).addEventListener('input', function () {
state[key] = parseInt(this.value, 10);
render();
});
}
slider('p', 'lP');
slider('lsize', 'lLBytes');
slider('owork', 'lO');
slider('bslide', 'lB');
slider('mslide', 'lM');
/* ---------- formatting ---------- */
function bytesLabel(lb) {
var b = Math.pow(2, lb);
if (b < 1024) return b + ' B';
if (b < 1024 * 1024) return (b / 1024) + ' KiB';
return (b / (1024 * 1024)) + ' MiB';
}
function nonceDescription() {
var alg = A.ALGORITHMS[state.alg];
if (state.nonceMode === 'random') {
return 'independently and uniformly generated ' + alg.r +
'-bit nonces from a CSPRNG';
}
if (state.nonceMode === 'implicit') {
return 'a non-repeating explicit nonce part for each message and a ' +
'fresh random implicit part for each key';
}
if (state.setting === 'mu' && !isAEGIS()) {
return 'non-repeating nonces and an independent random nonce offset ' +
'for each key';
}
if (state.setting === 'mu') {
return 'unique nonces and a unique identifier for each key encoded in ' +
'unused nonce bits';
}
return 'a unique nonce for each message';
}
function joinConditions(items) {
if (items.length === 1) return items[0];
if (items.length === 2) return items[0] + ' and ' + items[1];
return items.slice(0, -1).join(', ') + ', and ' + items[items.length - 1];
}
// Conservative authenticated-encryption advantage for using q protected
// messages and processing v forgery attempts together. Some multi-key
// formulas do not include generic offline key search, so add that floor
// unless the formula already contains an equal or larger no-usage term.
function operationalAdvantage(r, lq, lv) {
var bound = r.aea ? r.aea(lq, lv) : -Infinity;
var offline = state.lO - A.ALGORITHMS[state.alg].k;
var formulaFloor = r.aea ? r.aea(-Infinity, -Infinity) : -Infinity;
if (formulaFloor < offline - 1e-9) {
bound = A.log2sum(bound, offline);
}
return bound;
}
// The cards show the individual q and v boundaries. For application
// instructions, lower both by the same factor when necessary so using both
// recommended allowances together remains within the selected AEA target.
function operationalLimits(r) {
var qFinite = isFinite(r.qLimit);
var vFinite = isFinite(r.vLimit);
if (r.qLimit === -Infinity || r.vLimit === -Infinity ||
(qFinite && r.qLimit < 0) || (vFinite && r.vLimit < 0) ||
isNaN(r.qLimit) || isNaN(r.vLimit)) {
return null;
}
if (operationalAdvantage(r, -Infinity, -Infinity) >= state.lP) {
return null;
}
function advantageAt(reduction) {
return operationalAdvantage(
r,
qFinite ? r.qLimit - reduction : -Infinity,
vFinite ? r.vLimit - reduction : -Infinity
);
}
var reduction = 0;
if (advantageAt(0) > state.lP) {
var lo = 0, hi = 1;
while (advantageAt(hi) > state.lP && hi < 1024) hi *= 2;
for (var i = 0; i < 100; i++) {
var mid = (lo + hi) / 2;
if (advantageAt(mid) > state.lP) lo = mid;
else hi = mid;
}
reduction = hi;
}
return {
q: qFinite ? r.qLimit - reduction : Infinity,
v: vFinite ? r.vLimit - reduction : Infinity
};
}
// Return a count that never rounds above the calculated boundary.
function actionableCount(logCount) {
if (logCount <= 52) {
var count = Math.floor(Math.pow(2, logCount));
return count.toLocaleString('en-US') + ' (approximately ' +
A.fmtPow(logCount, 1) + ')';
}
var scaled = logCount * 10;
var nearest = Math.round(scaled);
var safeExponent = Math.abs(scaled - nearest) < 1e-9
? nearest / 10
: Math.floor(scaled) / 10;
return A.fmtPow(safeExponent, 1);
}
function recommendationText(r) {
var alg = state.alg;
var target = '2^' + state.lP;
var size = bytesLabel(state.lLBytes);
var fam = A.ALGORITHMS[state.alg].family;
var conditions = [alg];
if (isAEGIS()) conditions.push('a ' + state.tagBits + '-bit authentication tag');
conditions.push(nonceDescription());
if (state.setting === 'mu' && (fam === 'GCM' || fam === 'CCM')) {
conditions.push('no key processing more than ' + A.fmtPow(state.lB, 0) +
' encrypted ' + (fam === 'CCM' ? 'or decrypted ' : '') +
'128-bit blocks');
}
if (state.setting === 'mu' && state.nonceMode === 'random') {
conditions.push('no key protecting more than ' +
A.fmtPow(state.lM, 0) + ' messages');
}
conditions.push('plaintext plus AAD up to ' + size + ' per message');
var context = 'With ' + joinConditions(conditions);
if (state.nonceMode === 'random' && fam === 'CCM') {
return context + ', the selected nonce mode is not permitted. Select ' +
'Non-repeating before using this configuration.';
}
var limits = operationalLimits(r);
if (!limits) {
return context + ' cannot meet a ' + target + ' target. Choose a ' +
'larger-key or longer-tag cipher, or select a less stringent target.';
}
var text = context;
if (state.setting === 'su') {
var actions = [];
if (limits.q !== Infinity) {
actions.push('protect at most ' + actionableCount(limits.q) + ' messages');
}
if (limits.v !== Infinity) {
actions.push('allow at most ' + actionableCount(limits.v) +
' failed authentication attempts');
}
if (actions.length) {
text += ', to keep authenticated-encryption advantage at or below ' +
target + ', ' + actions.join(' and ') + ' under each key. ' +
(actions.length > 1
? 'Replace the key when either limit is reached.'
: 'Replace the key when that limit is reached.');
}
if (limits.q === Infinity) {
text += ' For successfully protected messages, follow your protocol\u2019s ' +
'normal key-usage limit.';
}
} else {
var totals = [];
if (limits.q !== Infinity) {
totals.push('protect at most ' + actionableCount(limits.q) +
' messages in total');
}
if (limits.v !== Infinity) {
totals.push('allow at most ' + actionableCount(limits.v) +
' failed authentication attempts in total');
}
if (totals.length) {
text += ', to keep authenticated-encryption advantage at or below ' +
target + ' across all keys, ' + totals.join(' and ') + '.';
}
if (limits.q === Infinity) {
text += ' For successfully protected messages, follow each protocol\u2019s ' +
'normal per-key usage limit.';
}
}
return text;
}
/* ---------- chart ---------- */
var NS = 'http://www.w3.org/2000/svg';
function el(tag, attrs) {
var e = document.createElementNS(NS, tag);
for (var k in attrs) e.setAttribute(k, attrs[k]);
return e;
}
function drawChart(r) {
var svg = $('chart');
while (svg.firstChild) svg.removeChild(svg.firstChild);
var W = 720, H = 340, m = { l: 56, r: 16, t: 18, b: 40 };
var iw = W - m.l - m.r, ih = H - m.t - m.b;
// x axis: log2 usage (messages). Choose a sensible window.
var qL = r.qLimit, vL = r.vLimit;
var finite = [qL, vL].filter(function (x) { return isFinite(x); });
var center = finite.length ? Math.max.apply(null, finite) : 40;
var x0 = Math.max(0, center - 30), x1 = center + 14;
// y axis: log2 advantage. Show from a little above 0 down to well below p.
var yTop = 0, yBot = Math.min(state.lP * 2, state.lP - 40, -80);
var yMin = Math.max(yBot, -300);
var YCLIP = 4; // draw values above 0 as just past the top edge (certainty)
function X(lq) { return m.l + (lq - x0) / (x1 - x0) * iw; }
function Y(la) {
var v = Math.max(yMin, Math.min(yTop, la));
return m.t + (yTop - v) / (yTop - yMin) * ih;
}
// For path drawing: values above 0 (advantage ~ certainty) are pinned just
// past the top edge so the line visibly leaves the plot instead of riding
// the axis.
function Yp(la) {
if (la > yTop) return m.t - YCLIP;
return Y(la);
}
// clip region so curves stop at the plot edge
var defs = el('defs', {});
var clip = el('clipPath', { id: 'plotclip' });
clip.appendChild(el('rect', { x: m.l, y: m.t, width: iw, height: ih }));
defs.appendChild(clip);
svg.appendChild(defs);
// gridlines + axis labels
var g = el('g', { 'font-size': '10', fill: '#93a4b5' });
svg.appendChild(g);
for (var gx = Math.ceil(x0 / 5) * 5; gx <= x1; gx += 5) {
svg.appendChild(el('line', { x1: X(gx), y1: m.t, x2: X(gx), y2: m.t + ih, stroke: '#22303d' }));
var t = el('text', { x: X(gx), y: m.t + ih + 16, 'text-anchor': 'middle' });
t.textContent = '2^' + gx; g.appendChild(t);
}
for (var gy = Math.ceil(yMin / 20) * 20; gy <= yTop; gy += 20) {
svg.appendChild(el('line', { x1: m.l, y1: Y(gy), x2: m.l + iw, y2: Y(gy), stroke: '#22303d' }));
var ty = el('text', { x: m.l - 6, y: Y(gy) + 3, 'text-anchor': 'end' });
ty.textContent = '2^' + gy; g.appendChild(ty);
}
// axis titles
var tx = el('text', { x: m.l + iw / 2, y: H - 6, 'text-anchor': 'middle', 'font-size': '11', fill: '#93a4b5' });
tx.textContent = 'usage (log2 messages)'; svg.appendChild(tx);
// target p line
svg.appendChild(el('line', { x1: m.l, y1: Y(state.lP), x2: m.l + iw, y2: Y(state.lP), stroke: '#ffb454', 'stroke-dasharray': '5 4', 'stroke-width': 1.5 }));
var tp = el('text', { x: m.l + iw - 4, y: Y(state.lP) - 5, 'text-anchor': 'end', 'font-size': '10', fill: '#ffb454' });
tp.textContent = 'target p = 2^' + state.lP; svg.appendChild(tp);
// curves
var curves = [];
function sample(fn, color, name) {
var d = '', started = false, N = 160;
for (var i = 0; i <= N; i++) {
var lq = x0 + (x1 - x0) * i / N;
var la = fn(lq);
if (!isFinite(la)) { started = false; continue; }
var px = X(lq), py = Yp(la);
d += (started ? 'L' : 'M') + px.toFixed(1) + ' ' + py.toFixed(1) + ' ';
started = true;
}
var path = el('path', { d: d, fill: 'none', stroke: color, 'stroke-width': 2, 'clip-path': 'url(#plotclip)' });
svg.appendChild(path);
curves.push({ color: color, name: name });
}
if (r.ca && r.ca(0) !== -Infinity) sample(function (lq) { return r.ca(lq); }, '#4da3ff', 'CA (confidentiality)');
if (r.ia) {
if (state.nonceMode === 'random' ||
(state.setting === 'su' && isGCM())) {
sample(function (lv) { return r.ia(lv, r.qLimit); }, '#37d0a0', 'IA (integrity; q at limit)');
} else if (state.setting === 'su' && isCCM()) {
sample(function (lv) { return r.ia(lv, Math.min(lv, r.qLimit)); }, '#37d0a0', 'IA (integrity)');
} else {
sample(function (lv) { return r.ia.length >= 2 ? r.ia(lv, lv) : r.ia(lv); }, '#37d0a0', 'IA (integrity)');
}
}
// limit markers
function mark(lim, color, label) {
if (!isFinite(lim)) return;
svg.appendChild(el('line', { x1: X(lim), y1: m.t, x2: X(lim), y2: m.t + ih, stroke: color, 'stroke-dasharray': '2 3' }));
var t = el('text', { x: X(lim), y: m.t + 10, 'text-anchor': 'middle', 'font-size': '9', fill: color });
t.textContent = label; svg.appendChild(t);
}
mark(qL, '#4da3ff', 'q limit');
mark(vL, '#37d0a0', 'v limit');
// legend
var leg = $('chartLegend');
leg.innerHTML = '';
curves.forEach(function (c) {
var s = document.createElement('span');
var i = document.createElement('i'); i.style.background = c.color;
s.appendChild(i); s.appendChild(document.createTextNode(c.name));
leg.appendChild(s);
});
var sp = document.createElement('span');
var ip = document.createElement('i'); ip.style.background = '#ffb454';
sp.appendChild(ip); sp.appendChild(document.createTextNode('target p'));
leg.appendChild(sp);
}
/* ---------- formulas panel ---------- */
function formulaText(result) {
var alg = A.ALGORITHMS[state.alg];
var fam = alg.family, mu = state.setting === 'mu';
var L = fam === 'ChaCha20-Poly1305'
? 'L\' <= ' + result.poly1305Blocks.toLocaleString('en-US') +
' Poly1305 data blocks'
: 'L = 2^' + (state.lLBytes - 4) + ' blocks';
var pSym = state.nonceMode === 'random' && result.collisionBudget !== -Infinity
? 'p_c'
: 'p';
var lines = [];
if (fam === 'AEGIS') {
lines.push('Parameters: k=' + alg.k + ', nonce=' + alg.r +
', tag=' + state.tagBits + ', effective online-forgery strength=' +
Math.min(state.tagBits, alg.differentialForgeryBits) +
', rate=' + (alg.rateBytes * 8) + ' bits; p = 2^' + state.lP +
'; o = 2^' + state.lO +
(mu && state.nonceMode === 'random' ? '; M = 2^' + state.lM : ''));
} else {
lines.push('Parameters: k=' + alg.k + ', n=' + alg.n + ', t=' + alg.t + ', r=' + alg.r +
'; ' + L + '; p = 2^' + state.lP + '; o = 2^' + state.lO +
(mu && (fam === 'GCM' || fam === 'CCM')
? '; ' + (isCCM() ? 'C' : 'B') + ' = 2^' + state.lB
: '') +
(mu && state.nonceMode === 'random' ? '; M = 2^' + state.lM : ''));
}
if (pSym === 'p_c') {
lines.push('Conditional-bound target: p_c = p/2 = 2^' + result.conditionalTarget);
}
lines.push('');
if (fam === 'AEGIS') {
lines.push('Claim-based model (' +
(state.nonceMode === 'random' ? 'conditional on no nonce repeat; ' : 'nonce-respecting; ') +
state.tagBits + '-bit tag):');
lines.push('Confidentiality: no q-dependent CA bound is stated by the cited sources');
lines.push(state.nonceMode === 'random'
? ' random-nonce collision risk supplies the finite q limit'
: ' q has no finite derived limit here');
var fb = Math.min(state.tagBits, alg.differentialForgeryBits);
lines.push('Integrity model: IA ≈ v / 2^' + fb +
(state.tagBits === 128 ? ' [AEGIS v1.1, Claim 1]' : ' [attack-specific]'));
lines.push(' v <= ' + pSym + ' * 2^' + fb);
lines.push('Combined model: AEA ≈ v/2^' + fb + ' + o/2^k');
if (alg.differentialForgeryBits < state.tagBits) {
lines.push('Trial condition: the 2^-216 differential path uses chosen-message');
lines.push(' source material plus an online forgery submission');
} else {
lines.push('Attack basis: ' + (state.tagBits === 128
? 'online tag guessing'
: '256-bit tag guessing; SSI24 finds no cheaper differential path'));
}
lines.push('Tag birthday: q ≈ 2^' + (state.tagBits / 2) + ' makes output collisions likely, but');
lines.push(' an output collision is not a fresh forgery or a state collision');
if (mu) {
lines.push('Multi-key: q and v are totals across keys; unique per-key nonce identifiers assumed');
}
} else if (!mu) {
if (fam === 'GCM') {
lines.push('Confidentiality: CA <= (s+q+1)^2 / 2^129');
lines.push(' q <= (sqrt(' + pSym + ')*2^64.5 - 1) / (L+1)');
lines.push('Integrity: IA <= v*(L+1)/2^128 * delta_128(s+q+v+1)');
lines.push(' delta_n(a) = (1 - (a-1)/2^n)^(-a/2), s <= q*L');
lines.push(' v is solved numerically at the displayed q; v <= 2^64');
} else if (fam === 'ChaCha20-Poly1305') {
lines.push('Confidentiality: CA <= 0 (no limit beyond ChaCha20 PRF security)');
lines.push('Poly1305: epsilon(L\') = 2^25*(L\'+1)');
lines.push('Integrity: IA <= v*epsilon(L\')/2^128');
lines.push(' v <= ' + pSym + '*2^128/epsilon(L\')');
lines.push(' (= ' + pSym + '*2^103/(L\'+1), exactly)');
} else if (alg.t === 128) {
lines.push('Confidentiality: CA <= (2L*q)^2 / 2^128');
lines.push(' q <= sqrt(' + pSym + ')*2^63 / L');
lines.push('Integrity: IA <= v/2^128 + (2L(v+q))^2 / 2^128');
lines.push(' v+q <= sqrt(' + pSym + ')*2^63 / L (simplified)');
} else {
lines.push('Confidentiality: CA <= (2L*q)^2 / 2^128');
lines.push(' q <= sqrt(' + pSym + ')*2^63 / L');
lines.push('Integrity: IA <= v/2^64 + (2L(v+q))^2 / 2^128');
lines.push(' split evenly: v <= ' + pSym + '*2^63, (2L(v+q))^2 <= ' + pSym + '*2^127');
}
lines.push('');
lines.push('All single-key bounds are additionally floored by offline key search: AEA <= o/2^k.');
} else {
if (fam === 'GCM' && state.nonceMode === 'implicit') {
lines.push('AEA <= ((q+v)*o + (q+v)^2)/2^(k+26) + (q+v)*L*B/2^127 [TLS 1.2 nonces]');
if (alg.k === 128) lines.push(' (o <= q+v): q+v <= min(sqrt(' + pSym + ')*2^76, ' + pSym + '*2^126/(L*B))');
else lines.push(' (k=256, first term negligible): q+v <= ' + pSym + '*2^127/(L*B)');
lines.push('CA: same form with q. IA <= AEA.');
} else if (fam === 'GCM') {
lines.push('AEA <= (q+v)*L*B / 2^127 [nonce randomization' +
(state.nonceMode === 'random' ? ', used as conditional model' : '') + ']');
lines.push('CA <= q*L*B / 2^127; IA <= AEA');
lines.push(' q+v <= ' + pSym + '*2^127/(L*B); per-value q,v assume equal proportions');
lines.push(' assumes B >> 100; AES-128-GCM also assumes o <= 2^70');
} else if (fam === 'ChaCha20-Poly1305') {
lines.push('Exact Theorem 7.2: epsilon(L\') = 2^25*(L\'+1)');
lines.push('AEA <= v*(epsilon(L\')+3)/2^128 + d*(o+q)/2^256');
lines.push(' + 2*o*256/2^256 + 2*v*768/2^256');
lines.push(' + (sigma+q)^2/2^513 + 2^-254 + 2^-254 + 2^(-96*delta)');
lines.push(' sigma <= q*' + result.chachaMessageBlocks +
' encrypted ChaCha20 blocks; delta/d are minimized numerically');
lines.push(' d = ceil((delta+1)*96 / max(1, 96-log2(q))) - 1');
lines.push('CA uses AEA(q,0); IA uses AEA(0,v); theorem query ranges are enforced');
} else {
lines.push('AEA <= (q+v)*L*C/2^127 + v/2^t + o/2^(k-6) [nonce randomization' +
(state.nonceMode === 'random' ? ', used as conditional model' : '') + ']');
lines.push(' (o <= q+v, even split): q+v <= ' + pSym + '*2^127/(L*C), v <= ' + pSym + '*2^(t-1)');
lines.push(' per-value q,v assume equal proportions');
}
}
if (state.nonceMode === 'random') {
lines.push('');
lines.push('Independent random nonces (overall target p):');
if (result.collisionBudget === -Infinity) {
lines.push(' M = 1, so a same-key nonce collision is impossible');
lines.push(' the full target p remains available to the conditional bound');
} else {
lines.push(' conditional security target = collision target = p/2 = 2^' +
result.collisionBudget);
lines.push(mu
? ' Pr[same-key repeat] <= q*(min(q,M)-1) / 2^' + (alg.r + 1)
: ' Pr[repeat] <= q*(q-1) / 2^' + (alg.r + 1));
lines.push(' collision allocation gives q <= ' + A.fmtPow(result.collisionLimit));
lines.push(' CA/IA/AEA <= corresponding conditional bound + Pr[repeat]');
}
if (result.randomNoncePerKeyCap !== undefined) {
lines.push(' specification guidance/cap: at most 2^' +
result.randomNoncePerKeyCap + ' messages per key');
}
}
return lines.join('\n');
}
function sourceText() {
var sources = [];
if (state.nonceMode === 'random') {
var fam = A.ALGORITHMS[state.alg].family;
if (fam === 'GCM') {
sources.push('<a href="https://doi.org/10.6028/NIST.SP.800-38D">NIST SP 800-38D, Sections 8.2.2 and 8.3</a>');
} else if (fam === 'ChaCha20-Poly1305') {
sources.push('<a href="https://www.rfc-editor.org/rfc/rfc8439.html#section-2.6">RFC 8439, Sections 2.6 and 4</a>');
} else if (fam === 'CCM') {
sources.push('<a href="https://doi.org/10.6028/NIST.SP.800-38C">NIST SP 800-38C</a>');
}
}
if (isAEGIS()) {
sources.push('<a href="https://datatracker.ietf.org/doc/draft-irtf-cfrg-aegis-aead/#section-10.1.1">AEGIS specification, Key and Nonce Selection</a>');
sources.push('<a href="https://competitions.cr.yp.to/round3/aegisv11.pdf">AEGIS v1.1, Section 4</a>');
sources.push('<a href="https://doi.org/10.1007/978-981-97-7737-2_1">256-bit-tag differential analysis (SSI24)</a>');
} else if (A.ALGORITHMS[state.alg].family === 'ChaCha20-Poly1305') {
sources.push('<a href="https://eprint.iacr.org/2023/085.pdf">Corrected ChaCha20-Poly1305 analysis, Theorems 3.2 and ' +
(state.setting === 'mu' ? '7.2' : '4.1') + '</a>');
}
return sources.length
? '<p class="muted">Sources: ' + sources.join('; ') + '.</p>'
: '';
}
/* ---------- render ---------- */
function render() {
// slider value labels
$('pVal').textContent = '2^' + state.lP;
$('lVal').textContent = bytesLabel(state.lLBytes) + ' (2^' + state.lLBytes + ')';
$('oVal').textContent = '2^' + state.lO;
$('bVal').textContent = '2^' + state.lB;
$('mVal').textContent = '2^' + state.lM;
var r = A.compute(params());
// plain-English operational guidance
$('recommendationText').textContent = recommendationText(r);
// cards
$('qBig').textContent = A.fmtPow(r.qLimit);
$('vBig').textContent = A.fmtPow(r.vLimit);
$('qSub').textContent = isFinite(r.qLimit) ? '≈ ' + A.fmtNum(r.qLimit) + ' messages' +
(r.qLimitReason ? ' (' + r.qLimitReason + ')' : '')
: (r.claimBased ? 'not bounded' : 'no message limit from CA');
$('vSub').textContent = isFinite(r.vLimit) ? '≈ ' + A.fmtNum(r.vLimit) + ' forgeries' +
(r.vLimitReason ? ' (' + r.vLimitReason + ')' : '')
: 'no forgery limit';
var off = state.lO - A.ALGORITHMS[state.alg].k;
$('offBig').textContent = A.fmtPow(off);
// warnings
var w = $('warnings');
w.innerHTML = '';
if (r.warnings.length) {
var d = document.createElement('div');
d.className = 'notice warn';
d.innerHTML = '<strong>Assumption warnings</strong><ul>' +
r.warnings.map(function (x) { return '<li>' + x + '</li>'; }).join('') + '</ul>';
w.appendChild(d);
}
if (r.notes.length) {
var dn = document.createElement('div');
dn.className = 'notice info';
dn.innerHTML = '<ul>' + r.notes.map(function (x) { return '<li>' + x + '</li>'; }).join('') + '</ul>';
w.appendChild(dn);
}
// comparison table
var tb = $('cmpTable').querySelector('tbody');
tb.innerHTML = '';
COMPARISON_ROWS.forEach(function (row) {
var name = row.name;
var rr = A.compute({
alg: name, setting: state.setting, nonceMode: state.nonceMode,
tagBits: state.tagBits, lP: state.lP, lLBytes: state.lLBytes,
lO: state.lO, lB: state.lB, lM: state.lM
});
var tr = document.createElement('tr');
if (name === state.alg) tr.className = 'sel';
function cell(txt, cls) { var td = document.createElement('td'); td.textContent = txt; if (cls) td.className = cls; return td; }
var binding = bindingOf(rr);
tr.appendChild(cell(row.label));
tr.appendChild(cell(A.fmtPow(rr.qLimit), 'num'));
tr.appendChild(cell(A.fmtPow(rr.vLimit), 'num'));
tr.appendChild(cell(binding));
tr.addEventListener('click', function () { selectAlgorithm(name); });
tb.appendChild(tr);
});
// chart + formulas
$('chartDesc').textContent = chartDesc(r);
drawChart(r);
$('fAlg').textContent = state.alg;
$('fSetting').textContent = state.setting === 'mu' ? 'multi-key' : 'single-key';
$('formulaBody').innerHTML = '<div class="fblock">' + formulaText(r) + '</div>' + sourceText();
}
function bindingOf(r) {
if (r.qLimitReason && (!isFinite(r.vLimit) || r.qLimit <= r.vLimit)) {
return r.qLimitReason;
}
if (!isFinite(r.qLimit) && isFinite(r.vLimit)) return 'integrity';
if (isFinite(r.qLimit) && !isFinite(r.vLimit)) return 'confidentiality';
if (!isFinite(r.qLimit) && !isFinite(r.vLimit)) return '—';
return r.qLimit <= r.vLimit ? 'confidentiality' : 'integrity';
}
function chartDesc(r) {
var s = 'Attacker advantage as usage grows. The curve crossing the dashed target line gives the limit. ';
if (state.nonceMode === 'random') {
s = 'Attacker advantage including same-key random-nonce collision risk. ' +
'The limit markers use separate p/2 budgets for the conditional bound and collision event, so they can precede the overall p crossing. ' +
'The integrity curve holds q at the displayed confidentiality limit while v grows. ';
}
if (state.nonceMode !== 'random' && state.setting === 'su' && isCCM()) {
s += 'For CCM the IA curve pairs each v with the largest admissible q.';
}
if (!isFinite(r.qLimit)) {
s += r.claimBased
? 'The cited AEGIS claims contain no q-dependent CA bound, so no CA curve is drawn.'
: 'Confidentiality imposes no finite limit here, so no CA curve is drawn.';
}
return s;
}
/* ---------- init ---------- */
// Allow deep-linking / testing via URL params, e.g.
// index.html?alg=AEAD_AEGIS128L&tag=256&setting=mu&nonce=random&p=-60&l=11&o=70&b=27&m=20
(function applyUrl() {
var q = new URLSearchParams(location.search);
if (q.get('alg') && A.ALGORITHMS[q.get('alg')]) state.alg = q.get('alg');
if (q.get('setting') === 'mu' || q.get('setting') === 'su') state.setting = q.get('setting');
if (q.get('nonce') === 'implicit' || q.get('nonce') === 'randomized' || q.get('nonce') === 'random') state.nonceMode = q.get('nonce');
if (q.get('tag') === '128' || q.get('tag') === '256') state.tagBits = parseInt(q.get('tag'), 10);
['p', 'l', 'o', 'b', 'm'].forEach(function (k) {
var v = parseFloat(q.get(k));
if (!isNaN(v)) {
if (k === 'p') state.lP = v;
if (k === 'l') state.lLBytes = v;
if (k === 'o') state.lO = v;
if (k === 'b') state.lB = v;
if (k === 'm') state.lM = v;
}
});
if (!isAEGIS()) state.tagBits = 128;
})();
syncControls();
render();
})();
</script>
</body>
</html>