aead-usage-limits / js /aead.js
jedisct1's picture
Use exact AES-GCM bounds and model random ChaCha20-Poly1305 nonces
949aa2a verified
Raw
History Blame Contribute Delete
34.9 kB
/*
* 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);