diff --git a/README.md b/README.md index 34e4bad..515f5fe 100644 --- a/README.md +++ b/README.md @@ -32,6 +32,12 @@ code/ check_cov_*.py ciphertext-only covariance-attack checks (referee M1) exp_real_sec.py stage G: real BERT WordPiece token streams verify_math.py closed-form checks V1-V11, PASS/FAIL and verify_math.csv + exp_learned.py every KM (lrn.) artifact, one function per stage + run_learned_reg.py runs those stages in order, sens before brute + merge_learned_rows.py folds the learned rows into the two table sources + report_learned.py every learned number beside its structured one + diag_whygap.py why a fixed key beats a learned one here + diag_jscc.py where a learned mask would win instead replot_security.py every result figure, from data/ to fig/ make_tables.py LaTeX rows of every result table, from data/ feasibility_security.py early CPU-sized study, kept for the record @@ -60,10 +66,19 @@ python exp_users_csi.py # load and channel-estimate sweeps python check_cov_attack.py # ciphertext-only covariance attack python diag_maskdegen.py # learned-key support degeneracy python check_family_enum.py # ciphertext-only enumeration of the key family +python run_learned_reg.py # every KM (lrn.) artifact, regularized keys +python merge_learned_rows.py # the learned rows of the two result tables python replot_security.py # all figures from the CSVs python make_tables.py # LaTeX rows of the result tables ``` +The learned keys are the regularized ones of Section V-C, trained under +the two penalties rather than under the cross-entropy alone. Training on +the cross-entropy alone drifts to disjoint sparse supports, which is an +orthogonal slot allocation rather than a superposition; `diag_whygap.py` +measures that drift and `exp_learned.learned_model` says why the +regularized keys are the ones every figure draws. + Seeds are fixed: training 1, evaluation 777, attacker key guess 20260813, key recovery 4242, brute-force search 31, cross-scheme comparison 11, key refresh 5150. Re-running reproduces the released CSV @@ -82,17 +97,17 @@ Logarithms in an entropy or an information rate are base two. | Artifact | Script | Data | |---|---|---| -| Fig. 2 SER against SNR | `exp_full.stage_A` | `sec_snr.csv` | -| Fig. 3 key length | `exp_full.stage_B` | `sec_keylen.csv` | -| Fig. 4 jamming (4 schemes) | `exp_full.stage_C`, `stage_L` | `sec_jam_cmp.csv`, `sec_jam.csv` | -| Fig. 5 key sensitivity | `exp_full.stage_I` | `sec_sens_cmp.csv` | -| Fig. 6 brute-force search | `exp_full.stage_I`, `stage_F`, `stage_J` | `sec_brute_cmp.csv`, `sec_brute.csv` | -| Fig. 7 known-plaintext attack | `exp_kpa`, `exp_permkpa` | `kpa.csv`, `pkpa.csv` | -| Fig. 8 real token streams | `exp_real_sec` | `real_sec_ter.csv` | -| Scheme comparison table | `exp_full.stage_E` | `sec_compare.csv` | -| Key family table | `exp_full.stage_D` | `sec_maskfam.csv`, `sec_regjam.csv` | -| Headline recovery table | `exp_real_sec` | `real_sec_stats.json` | -| Key refresh tables | `exp_refresh` | `refresh_summary.csv`, `refresh_kpa.csv` | +| Fig. 2 SER against SNR | `exp_full.stage_A`, `stage_N` | `sec_snr.csv`, `sec_snr_learned.csv` | +| Fig. 3 key length | `exp_full.stage_B`, `exp_learned.keylen`, `.keylen_perm` | `sec_keylen.csv`, `sec_keylen_learned.csv`, `sec_keylen_perm.csv` | +| Fig. 4 jamming (5 curves) | `exp_full.stage_C`, `stage_L`, `exp_learned.jamming` | `sec_jam_cmp.csv`, `sec_jam.csv`, `sec_jam_learned.csv` | +| Fig. 5 key sensitivity | `exp_full.stage_I`, `exp_learned.sens` | `sec_sens_cmp.csv`, `sec_sens_learned.csv` | +| Fig. 6 brute-force search | `exp_full.stage_I`, `stage_F`, `stage_J`, `exp_learned.brute` | `sec_brute_cmp.csv`, `sec_brute.csv`, `sec_brute_learned.csv` | +| Fig. 7 known-plaintext attack | `exp_kpa`, `exp_permkpa`, `exp_learned.kpa` | `kpa.csv`, `pkpa.csv`, `kpa_learned.csv` | +| Fig. 8 real token streams | `exp_real_sec`, `exp_learned.real` | `real_sec_ter.csv`, `real_sec_ter_learned.csv` | +| Scheme comparison table | `exp_full.stage_E`, `exp_learned.compare`, `merge_learned_rows` | `sec_compare.csv` | +| Key families (Sec. VI-G prose) | `exp_full.stage_D` | `sec_maskfam.csv`, `sec_regjam.csv` | +| Headline recovery (Sec. VI-H prose) | `exp_real_sec` | `real_sec_stats.json` | +| Key refresh table | `exp_refresh`, `exp_learned.refresh`, `merge_learned_rows` | `refresh_summary.csv`, `refresh_kpa.csv` | | Information-theoretic leakage | `exp_infotheory` | `infotheory.csv` | | Semantic similarity | `exp_semantic` | `semantic.csv` | | Load and channel-estimate sweeps | `exp_users_csi` | `users.csv`, `csi.csv` | @@ -103,6 +118,8 @@ Run one stage on its own with `python code/exp_full.py stage_B`, or the whole ch | Covariance attack (Sec. IV) | `check_cov_attack` | `cov_attack.csv` | | Learned-key degeneracy (Sec. VI-F) | `diag_maskdegen` | `maskdegen.csv` | | Closed-form and symbolic checks | `verify_math` | `verify_math.csv` | +| Why a fixed key wins here (not in the paper) | `diag_whygap` | `whygap.csv` | +| Where a learned mask would win (not in the paper) | `diag_jscc` | `jscc.csv` | ## Security scope diff --git a/code/check_consistency.py b/code/check_consistency.py index a02a8eb..babe4b0 100644 --- a/code/check_consistency.py +++ b/code/check_consistency.py @@ -55,8 +55,11 @@ chk("24 and 35 in tex", "$24$ to\n$35$~percent" in tex or "$24$ to $35$~percent" ew = [float(x["eve_wrong"]) for x in sn] ch = float(sn[0]["chance"]) dev = max(abs(x - ch) for x in ew) -chk("outsider at chance to 3.5e-4", dev < 3.6e-4, "max deviation %.2e" % dev) -chk("3.5e-4 in tex", "$3.5\\times10^{-4}$" in tex, "searched tex", +_ewl = [float(x["eve_wrong"]) for x in rows("sec_snr_learned.csv")] +dev = max(dev, max(abs(x - ch) for x in _ewl)) +chk("outsider at chance to 4e-4, both families", dev < 4.0e-4, + "max deviation %.2e" % dev) +chk("4e-4 in tex", "$4\\times10^{-4}$" in tex, "searched tex", needs_tex=True) # the main configuration's legitimate rate, the reference every later @@ -212,8 +215,8 @@ chk("learned support overlap 0.10", round(float(md["learned"]["mean_overlap"]), 2) == 0.10, md["learned"]["mean_overlap"]) chk("degeneracy numbers in tex", - "$5$ to $8$ of the $64$ entries" in tex - and "overlapping by $0.10$" in " ".join(tex.split()), + "$5$ to $8$ of the $64$ entries" in " ".join(tex.split()) + and "only $0.10$ of the smaller of any two such sets" in " ".join(tex.split()), "searched tex", needs_tex=True) # --- why the permutation key is granted a shared permutation --------- @@ -224,7 +227,7 @@ chk("per-user permutation legitimate rate", abs(pv["per_user"] - 0.129) < 1e-3, "%.5f" % pv["per_user"]) if HAVE_TEX: chk("quoted permutation cost in tex", - "from $0.053$ to $0.129$" in " ".join(tex.split()), + "at $0.129$ against $0.053$" in " ".join(tex.split()), "searched tex", needs_tex=True) @@ -299,22 +302,40 @@ chk("learned family tracks the structured one over the SNR range", for r in _sl), max(float(r["legit"]) / _sn[float(r["snr_db"])] for r in _sl))) -chk("learned 0.064 at 10 dB", - abs([float(r["legit"]) for r in _sl - if float(r["snr_db"]) == 10.0][0] - 0.064) < 5e-4, - "%.5f" % [float(r["legit"]) for r in _sl - if float(r["snr_db"]) == 10.0][0]) +_l10 = [float(r["legit"]) for r in _sl if float(r["snr_db"]) == 10.0][0] +chk("learned 0.061 at 10 dB", abs(_l10 - 0.061) < 5e-4, "%.5f" % _l10) +# The learned curves must be the regularized keys, not the unpenalized +# ones. Both are measured in sec_maskfam.csv and they differ by 0.003, +# which is larger than the spread of either, so matching the right row +# pins which family every figure draws. Cross-entropy alone drifts to a +# slot allocation whose key space is a support rather than a sphere, so +# drawing it would not support the key-space claim. +_fam = {r["family"]: float(r["legit_ser"]) for r in rows("sec_maskfam.csv")} +chk("the plotted learned family is the regularized one", + abs(_l10 - _fam["learned_reg"]) < abs(_l10 - _fam["learned"]) + and abs(_l10 - _fam["learned_reg"]) < 1.5e-3, + "plotted %.5f, reg %.5f, plain %.5f" + % (_l10, _fam["learned_reg"], _fam["learned"])) _bl = {int(r["K"]): float(r["ser_mask"]) for r in rows("sec_brute_learned.csv")} chk("learned key resists a million random guesses", - abs(_bl[1_000_000] - 0.72) < 5e-3, "%.4f" % _bl[1_000_000]) + abs(_bl[1_000_000] - 0.71) < 5e-3, "%.4f" % _bl[1_000_000]) _kl = {(float(r["snr_db"]), int(r["n_frames"])): float(r["eve_ser"]) for r in rows("kpa_learned.csv")} chk("learned key falls to known plaintext like the structured one", _kl[(10.0, 4)] < 0.08 and _kl[(10.0, 1)] > 0.9, "N=1 %.3f, N=4 %.4f" % (_kl[(10.0, 1)], _kl[(10.0, 4)])) +_rs = {float(r["snr_db"]): float(r["ter_legit"]) + for r in rows("real_sec_ter.csv")} +_rl = {float(r["snr_db"]): float(r["ter_legit"]) + for r in rows("real_sec_ter_learned.csv")} +_rat = [_rl[k] / _rs[k] for k in _rs] +chk("learned keeps its uniform-source distance on real text", + all(1.10 < v < 1.25 for v in _rat), + "ratio %.2f to %.2f" % (min(_rat), max(_rat))) + # --- trends, which the value assertions above cannot see --------------- _snr = rows("sec_snr.csv") _lg = [float(r["legit"]) for r in _snr] @@ -387,7 +408,8 @@ for _k, _c in [("V8 cross-period remainder", 0.0005), and float(_vm[_k]["abs_err"]) <= _c, _vm[_k]["empirical"] if _k in _vm else "row missing") chk("format-matched OMA quoted as 0.055", - "$0.055$ at $10$~dB against the proposed" in tex, "Section VI-B", + "$0.055$ at $10$~dB" in tex and "the proposed $0.053$" in tex, + "Section VI-B", needs_tex=True) # --- tables against their generator ----------------------------------- diff --git a/code/check_family_enum.py b/code/check_family_enum.py index 5f3e8d9..50a4ea2 100644 --- a/code/check_family_enum.py +++ b/code/check_family_enum.py @@ -28,7 +28,7 @@ from pathlib import Path import torch -from exp_full import MAIN_D, base_keys, get_model, main_model +from exp_full import MAIN_D, base_keys, get_model_reg, main_model from sse_lib import DEVICE, rayleigh_gain, snr_to_sigma2, write_csv DATA = Path(__file__).resolve().parents[1] / "data" @@ -117,7 +117,9 @@ def run(): torch.manual_seed(SEED) rows = [] _sweep(main_model(), "structured", rows) # keys frozen to Walsh - _sweep(get_model(P=4, vu=16, d=MAIN_D, U=4, iters=4000, seed=1), + # the regularized keys of Section V-C, which are the learned family + # every figure draws; the unpenalized ones are a slot allocation + _sweep(get_model_reg(P=4, vu=16, d=MAIN_D, U=4, iters=4000, seed=1), "learned", rows) # keys trained in R^L write_csv(DATA / "family_enum.csv", ["family", "keying", "snr_db", "n_frames", diff --git a/code/diag_jscc.py b/code/diag_jscc.py new file mode 100644 index 0000000..c9d9c57 --- /dev/null +++ b/code/diag_jscc.py @@ -0,0 +1,181 @@ +# -*- coding: utf-8 -*- +"""Does learning the mask buy anything, and where would it? + +Nothing in the manuscript rests on this; it answers a design question +the paper does not raise. Output in data/jscc.csv. + +What the run found, at 10 dB over three training seeds. A: with one +user the mask does not matter, a Walsh row and no mask at all landing +at 0.0133 and 0.0132 against 0.0148 for a learned key, so the mask +carries no part of the source-channel map and only separates users. +B: at the six key lengths where truncated Walsh rows are not exactly +orthogonal, learning wins once, at L=14 with kappa 0.048, and loses at +L=16, 20, 22 and 24 where the rows ARE orthogonal. C: raising the load +to U=20 at L=16 gives learning its second win, by 1e-5 on an error +rate of 0.9998, which is no win at all because both families have +already collapsed. Every other point is a tie inside the seed spread. + +So the room for a learned mask is real but narrow, and it is where +exact orthogonality does not exist rather than where the load is high. +A learned mask beating a fixed one wants a loss that is not digit +cross-entropy, or a source that is not uniform, or a channel that is +not a scalar the receiver divides out. + +A joint source-channel view says a learned mask should beat a fixed one, +since the fixed one lies inside the search space. It does not here, and +these experiments say why, and where the picture changes. + +A. What job does the mask actually do? Run one user. With a single user + there is nobody to separate from, so if the mask carried any part of + the source-channel map its choice would still matter. Compare a Walsh + row, no mask at all, and a learned key, each with the codebook + trained around it. Equal error rates mean the mask is not part of + that map: the codebook is, and the mask only separates users. This + also has a one-line proof. A unit-modulus key has m^2 = 1, so it + cancels from the signal self-term and from the noise projection + alike, and the score is unchanged. + +B. Where is the structured family no longer optimal? The construction + supplies exactly orthogonal unit-modulus rows only at the lengths + where truncation preserves orthogonality. At L = 6, 10, 14, 18, 20 + and 22 the truncated rows correlate, so no exactly orthogonal + unit-modulus family is available and learning has room to find a + better packing. Every length is run at several seeds, because a + single training run is not evidence of a family being better. + +C. Overload. Beyond U = L - 1 no orthogonal set of non-constant rows + exists at all, so the structured family has to reuse rows and the + comparison is decided by whatever packing learning finds. + +Run on a GPU host: python code/diag_jscc.py +""" +from __future__ import annotations + +import csv +import statistics +import sys +from pathlib import Path + +import torch + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from exp_full import (MAIN_D, base_keys, get_model_reg, # noqa: E402 + mean_abs_xcorr) +from sse_lib import DATA, DEVICE, SSE, eval_ser_sse, set_seed # noqa: E402 +import sse_lib as L # noqa: E402 + +SNR = 10.0 +SEEDS = [1, 2, 3] +FRAMES = 400_000 + + +def train_with_key(W0, d, P, vu, U, iters=4000, seed=1, frozen=True): + """Train the codebook around a given key, optionally holding it.""" + set_seed(seed) + m = SSE(P=P, vu=vu, d=d, users=U).to(DEVICE) + with torch.no_grad(): + m.W.copy_(W0.to(DEVICE)) + m.W.requires_grad_(not frozen) + L.train_sse(m, iters=iters, batch=256, lr=3e-3, seed=seed) + m.calibrate_power() + return m + + +def ms(vals): + """Mean and, when there is more than one, the sample spread.""" + if len(vals) == 1: + return vals[0], 0.0 + return statistics.mean(vals), statistics.stdev(vals) + + +def part_a(rows): + """One user: does the choice of mask matter at all?""" + print("-- A. one user, d=%d, L=%d, %d seeds --" + % (MAIN_D, MAIN_D // 4, len(SEEDS))) + Lp = MAIN_D // 4 + cases = [("Walsh row", base_keys(1, Lp), True), + ("all ones (no mask)", torch.ones(1, Lp), True), + ("learned, free", base_keys(1, Lp), False)] + for name, W0, frozen in cases: + v = [eval_ser_sse(train_with_key(W0, MAIN_D, 4, 16, 1, seed=s, + frozen=frozen), + [SNR], frames=FRAMES)[0] for s in SEEDS] + mu, sd = ms(v) + print(" %-20s SER %.5f +- %.5f" % (name, mu, sd)) + rows.append(["A one user", name, "%.5f" % mu, "%.5f" % sd, "", ""]) + + +def part_b(rows): + """Key lengths where no exactly orthogonal unit-modulus family exists.""" + print("\n-- B. key length, U=4, %d seeds --" % len(SEEDS)) + print(" %-4s %-9s %-18s %-18s %s" + % ("L", "kappa str", "structured SER", "learned SER", "verdict")) + for Lp in [6, 8, 10, 12, 14, 16, 18, 20, 22, 24]: + d = 4 * Lp + try: + W0 = base_keys(4, Lp) + except ValueError as e: + print(" %-4d skipped: %s" % (Lp, e)) + continue + ks = mean_abs_xcorr(W0) + vs = [eval_ser_sse(train_with_key(W0, d, 4, 16, 4, seed=s), + [SNR], frames=FRAMES)[0] for s in SEEDS] + vl = [eval_ser_sse(get_model_reg(P=4, vu=16, d=d, U=4, iters=4000, + seed=s), + [SNR], frames=FRAMES)[0] for s in SEEDS] + (mus, sds), (mul, sdl) = ms(vs), ms(vl) + # a win only counts when it clears the spread of both runs + win = "learned" if mul + sdl < mus - sds else ( + "structured" if mus + sds < mul - sdl else "tie") + print(" %-4d %-9.5f %.5f +- %.5f %.5f +- %.5f %s" + % (Lp, ks, mus, sds, mul, sdl, win)) + rows.append(["B key length", "L=%d" % Lp, "%.5f" % mus, + "%.5f" % sds, "%.5f" % mul, "%.5f/%s" % (ks, win)]) + + +def part_c(rows): + """Overload: more users than the construction has orthogonal rows.""" + print("\n-- C. load at L=16, %d seeds --" % len(SEEDS)) + Lp, d = 16, 64 + print(" %-4s %-9s %-18s %-18s %s" + % ("U", "kappa str", "structured SER", "learned SER", "verdict")) + for U in [4, 8, 12, 15, 16, 20]: + try: + W0 = base_keys(U, Lp) + except ValueError: + # beyond the orthogonal rows the construction has to reuse + # them, which is the honest structured fallback + H = base_keys(Lp - 1, Lp) + W0 = H[[i % (Lp - 1) for i in range(U)]] + ks = mean_abs_xcorr(W0) + vs = [eval_ser_sse(train_with_key(W0, d, 4, 16, U, seed=s), + [SNR], frames=FRAMES)[0] for s in SEEDS] + vl = [eval_ser_sse(get_model_reg(P=4, vu=16, d=d, U=U, iters=4000, + seed=s), + [SNR], frames=FRAMES)[0] for s in SEEDS] + (mus, sds), (mul, sdl) = ms(vs), ms(vl) + win = "learned" if mul + sdl < mus - sds else ( + "structured" if mus + sds < mul - sdl else "tie") + print(" %-4d %-9.5f %.5f +- %.5f %.5f +- %.5f %s" + % (U, ks, mus, sds, mul, sdl, win)) + rows.append(["C load", "U=%d" % U, "%.5f" % mus, "%.5f" % sds, + "%.5f" % mul, "%.5f/%s" % (ks, win)]) + + +def main(): + print("device", DEVICE) + rows = [] + part_a(rows) + part_b(rows) + part_c(rows) + out = DATA / "jscc.csv" + with open(out, "w", newline="") as f: + w = csv.writer(f) + w.writerow(["part", "case", "structured_ser", "structured_sd", + "learned_ser", "kappa_and_verdict"]) + w.writerows(rows) + print("\n[csv]", out) + + +if __name__ == "__main__": + main() diff --git a/code/diag_whygap.py b/code/diag_whygap.py new file mode 100644 index 0000000..248ca90 --- /dev/null +++ b/code/diag_whygap.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +"""Why do structured keys beat learned ones on the legitimate error rate? + +The gap is 0.053 against 0.064 at 10 dB, and a reader may reasonably +suspect that the learned keys are handicapped, since they alone are +trained under the channel while the structured ones are fixed by +construction. This measures where the gap comes from. + +Three questions, one experiment each. + +1. Is the gap a channel-adaptation failure? If it were, the two families + would differ by more at some channel qualities than at others. The + ratio across the SNR sweep answers this from data already on disk. + +2. Is the structured key a point that training can improve on? Start + training from the Walsh-Hadamard keys with the masks unfrozen and let + Adam move them. If the structured point is a genuine optimum, the + error rate stays or rises; if training is merely under-converged from + its random start, it falls. + +3. What does the learned key lose? Two candidates, measured directly: + residual cross-user correlation, which the analysis names as the + first-order leakage and interference term, and departure from unit + modulus, which spreads the key energy unevenly across the entries so + that a digit is decided over an effectively shorter support. + +Run: python code/diag_whygap.py +""" +from __future__ import annotations + +import csv +import math +import sys +from pathlib import Path + +import torch + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +from exp_full import (MAIN_D, base_keys, get_model, main_model, # noqa: E402 + mean_abs_xcorr) +from sse_lib import DATA, DEVICE, eval_ser_sse # noqa: E402 +import sse_lib as L # noqa: E402 + +FRAMES = 300_000 +SNR = 10.0 + + +def modulus_stats(W): + """How far the key entries are from unit modulus, per user. + + A unit-modulus key puts the same energy on every entry, so the + signal term does not depend on the key and every entry of the period + carries its share of the decision. The ratio below is the effective + fraction of the L entries the key actually uses, by the + participation ratio (sum a^2)^2 / (L sum a^4) with a the entry + magnitudes. It is one for a unit-modulus key and 1/L for a key that + puts everything on one entry. + """ + a2 = W.pow(2) + pr = a2.sum(dim=1).pow(2) / (W.shape[1] * a2.pow(2).sum(dim=1)) + return pr + + +def report(name, model, rows): + W = model.masks().detach().cpu() + ser = eval_ser_sse(model, [SNR], frames=FRAMES)[0] + kap = mean_abs_xcorr(model.masks().detach()) + pr = modulus_stats(W) + print("%-24s SER %.5f kappa-bar %.5f entry use %.3f" + % (name, ser, kap, float(pr.mean()))) + rows.append([name, "%.5f" % ser, "%.5f" % kap, "%.4f" % float(pr.mean())]) + return ser + + +def main(): + rows = [] + print("main configuration d=%d, L=%d, 10 dB, %d frames\n" + % (MAIN_D, MAIN_D // 4, FRAMES)) + + print("-- the two families as the paper plots them --") + fix = main_model() + s_fix = report("structured (frozen)", fix, rows) + free = get_model(iters=4000) + s_free = report("learned (free start)", free, rows) + + print("\n-- question 2: can training improve on the structured key? --") + # same trainer, same iterations, same seed, but the masks start at + # the Walsh-Hadamard point and are free to move + from sse_lib import SSE, set_seed + set_seed(1) + m = SSE(P=4, vu=16, d=MAIN_D, users=4).to(DEVICE) + with torch.no_grad(): + m.W.copy_(base_keys(4, MAIN_D // 4).to(DEVICE)) + m.W.requires_grad_(True) + L.train_sse(m, iters=4000, batch=256, lr=3e-3, seed=1) + m.calibrate_power() + s_warm = report("learned (Walsh start)", m, rows) + + print("\nreading:") + print(" free start %+.1f percent against the structured key" + % (100.0 * (s_free - s_fix) / s_fix)) + print(" Walsh start %+.1f percent against the structured key" + % (100.0 * (s_warm - s_fix) / s_fix)) + if s_warm > s_fix: + print(" training moves off the structured point and pays for it,") + print(" so the structured key is not a point learning improves on.") + else: + print(" training improves on the structured point, so the gap is") + print(" under-convergence from the random start, not geometry.") + + out = DATA / "whygap.csv" + with open(out, "w", newline="") as f: + w = csv.writer(f) + w.writerow(["family", "legit_ser", "kappa_bar", "entry_use"]) + w.writerows(rows) + print("\n[csv]", out) + + +if __name__ == "__main__": + main() diff --git a/code/exp_full.py b/code/exp_full.py index a2f697b..0ff484c 100644 --- a/code/exp_full.py +++ b/code/exp_full.py @@ -194,11 +194,14 @@ def stage_N(): The structured family is enumerable and closed under the elementwise product, the learned one is neither, so the paper reports both. This stage runs the same SNR sweep as stage_A with the keys trained in - R^L instead of frozen to Walsh-Hadamard rows, at the same frame - count, so the two are directly comparable. + R^L under the regularized loss instead of frozen to Walsh-Hadamard + rows, at the same frame count, so the two are directly comparable. """ print("[N] security vs SNR, learned key family ...") - m = get_model(P=4, vu=16, d=MAIN_D, U=4, iters=4000, seed=1) + # the regularized loss of Section V-C, not the cross-entropy alone: + # see exp_learned.learned_model for why the unpenalized keys are not + # the family the paper claims + m = get_model_reg(P=4, vu=16, d=MAIN_D, U=4, iters=4000, seed=1) snr = [float(v) for v in range(0, 21, 2)] frames = 800_000 legit = eval_ser_sse(m, snr, frames=frames) diff --git a/code/exp_learned.py b/code/exp_learned.py index d955115..a0662e2 100644 --- a/code/exp_learned.py +++ b/code/exp_learned.py @@ -11,7 +11,8 @@ ones. Every evaluation mirrors its structured counterpart exactly: same SNR, same frame counts, same seeds, same evaluators. Only the key family -differs. +differs. The learned keys are the regularized ones of Section V-C, not +the unpenalized ones: see learned_model below for why. """ from __future__ import annotations @@ -21,16 +22,28 @@ from pathlib import Path import torch import exp_kpa +import exp_refresh from exp_full import (MAIN_D, eval_ser_eve, eval_ser_jam, eve_wrong_mask, - get_model, mean_abs_xcorr, oma_ser_keylen) + get_model_reg, mean_abs_xcorr, oma_ser_keylen) from sse_lib import DATA, DEVICE, eval_ser_sse, write_csv SEED = 1 def learned_model(d=MAIN_D, P=4, vu=16, U=4, iters=4000, seed=SEED): - """The learned counterpart of main_model: same everything, keys free.""" - return get_model(P=P, vu=vu, d=d, U=U, iters=iters, seed=seed) + """The learned counterpart of main_model: same everything, keys free. + + The keys are trained under the two penalties of the regularized loss + rather than under the cross-entropy alone. Cross-entropy on its own + has an attractor at disjoint sparse supports, which is an orthogonal + slot allocation: the keys it reaches carry 99 percent of their + energy on about six of the L entries, so a digit is decided over a + sixth of its period and the key set is a choice of support rather + than a dense direction in R^L. The penalties are the design of + Section V-C and hold that drift back, which is the realization the + paper claims for the learned family. + """ + return get_model_reg(P=P, vu=vu, d=d, U=U, iters=iters, seed=seed) def keylen(): @@ -56,7 +69,7 @@ def jamming(): """Fig. 4's learned curves.""" print("[learned] jamming ...") m = learned_model() - jsr = [-10.0, -5.0, 0.0, 5.0, 10.0, 15.0, 20.0] + jsr = [float(v) for v in range(-10, 21, 2)] # the grid Fig. 4's other curves use blind = eval_ser_jam(m, 10.0, jsr, frames=500_000, mode="blind", target=0) matched = eval_ser_jam(m, 10.0, jsr, frames=500_000, mode="matched", target=0) @@ -106,7 +119,7 @@ def refresh(): W0, B0 = m.W.detach().clone(), m.B.detach().clone() base = eval_ser_sse(m, [10.0], frames=300_000)[0] out = [] - for b in range(8): + for b in range(exp_refresh.BLOCKS): g = torch.Generator(device=DEVICE).manual_seed(5150 + b) xi = torch.randperm(m.L, generator=g, device=DEVICE) eps = torch.randint(2, (m.L,), generator=g, device=DEVICE) * 2.0 - 1.0 @@ -133,11 +146,15 @@ def compare(): print("[learned] scheme comparison ...") m = learned_model() F = 300_000 - legit = eval_ser_sse(m, [10.0], frames=F)[0] - out = eval_ser_eve(m, eve_wrong_mask(m.users, m.L, - seed=20260813).to(DEVICE), - [10.0], frames=F)[0] - ins = eval_ser_eve(m, m.masks().detach().roll(1, 0), [10.0], frames=F)[0] + # the table caption states a user-1 convention and every structured + # row honours it, so this row uses the same evaluator rather than the + # four-user average eval_ser_eve returns + from exp_full import eval_scheme + legit = eval_scheme(m, 10.0, F) + out = eval_scheme(m, 10.0, F, + rx_masks=eve_wrong_mask(m.users, m.L, + seed=20260813).to(DEVICE)) + ins = eval_scheme(m, 10.0, F, rx_masks=m.masks().detach().roll(1, 0)) jam = eval_ser_jam(m, 10.0, [0.0], frames=F, mode="blind", target=0)[0] write_csv(DATA / "compare_learned.csv", ["scheme", "legit_ser", "eve_out", "eve_in", "jam0_ser"], @@ -235,3 +252,29 @@ def real(): for n, blob in saved.items(): (DATA / n).write_bytes(blob) print(" learned artifacts written, structured ones restored") + + +def keylen_perm(): + """Fig. 3's permutation-key curves. + + The permutation scheme keeps the masks public and hides the frame + order instead, so its legitimate receiver inverts the permutation + and decodes as the public-mask receiver does, while its eavesdropper + holds the public masks but not the order. Both are swept over the + same key lengths as the structured family so the figure carries the + comparison scheme at every point rather than only at L = 64. + """ + import torch + from exp_full import (eval_scheme, eval_scheme_permuted_eve, main_model) + print("[learned] key length, permutation key ...") + rows = [] + for d in [32, 48, 64, 80, 96, 128, 192, 256]: + m = main_model(d=d) + gp = torch.Generator().manual_seed(11) + perms = torch.randperm(d, generator=gp)[None].repeat(m.users, 1) + lg = eval_scheme(m, 10.0, 500_000, perms=perms) + ev = eval_scheme_permuted_eve(m, 10.0, 500_000, perms) + rows.append((m.L, d, lg, ev)) + print(" L=%3d legit %.4f eve %.4f" % (m.L, lg, ev)) + write_csv(DATA / "sec_keylen_perm.csv", + ["L", "d", "legit_ser", "eve_ser"], rows) diff --git a/code/merge_learned_rows.py b/code/merge_learned_rows.py new file mode 100644 index 0000000..ad44ecf --- /dev/null +++ b/code/merge_learned_rows.py @@ -0,0 +1,73 @@ +# -*- coding: utf-8 -*- +"""Fold the learned-key rows into the two table sources. + +Table IV reads sec_compare.csv and Table V reads refresh_summary.csv, +and both are written by the structured stages, which know nothing about +the learned family. Its rows were appended by hand, so a rerun of the +learned stages left the tables behind. This does the fold, so both files +are derived from data/ like every other table source. + +Run after code/run_learned_reg.py, before code/make_tables.py. +""" +from __future__ import annotations + +import csv +from pathlib import Path + +DATA = Path(__file__).resolve().parents[1] / "data" + + +def read(name): + with open(DATA / name) as f: + r = csv.DictReader(f) + return r.fieldnames, list(r) + + +def write(name, fields, rows): + with open(DATA / name, "w", newline="") as f: + w = csv.DictWriter(f, fieldnames=fields) + w.writeheader() + w.writerows(rows) + + +def upsert(rows, key, value, row): + """Replace the row carrying key==value, or append it.""" + for i, r in enumerate(rows): + if r[key] == value: + rows[i] = row + return rows + rows.append(row) + return rows + + +def main(): + # Table IV: the learned scheme row, measured by exp_learned.compare + fields, rows = read("sec_compare.csv") + _, learned = read("compare_learned.csv") + assert len(learned) == 1, "compare_learned.csv should carry one row" + rows = upsert(rows, "scheme", "proposed_learned", + {k: learned[0][k] for k in fields}) + write("sec_compare.csv", fields, rows) + print("sec_compare.csv proposed_learned jam0 %s" + % learned[0]["jam0_ser"]) + + # Table V: the learned refresh row, averaged over the blocks that + # exp_learned.refresh measured, at the same entropy as the + # structured refresh because the invariance group is the same + fields, rows = read("refresh_summary.csv") + _, blocks = read("refresh_learned.csv") + lg = sum(float(r["legit_ser"]) for r in blocks) / len(blocks) + ev = sum(float(r["eve_ser"]) for r in blocks) / len(blocks) + ent = next(r["entropy_bits"] for r in rows + if r["scheme"] == "Invariant, KM (str.)") + rows = upsert(rows, "scheme", "Invariant, KM (lrn.)", + {"scheme": "Invariant, KM (lrn.)", + "legit": "%.6f" % lg, "eve": "%.6f" % ev, + "entropy_bits": ent}) + write("refresh_summary.csv", fields, rows) + print("refresh_summary.csv Invariant, KM (lrn.) legit %.5f eve %.5f" + % (lg, ev)) + + +if __name__ == "__main__": + main() diff --git a/code/replot_security.py b/code/replot_security.py index 6bbab05..411a262 100644 --- a/code/replot_security.py +++ b/code/replot_security.py @@ -365,7 +365,18 @@ def fig_keylen(): ax.semilogy(col(rl, "L", int), col(rl, "legit_ser"), **STY["km_lrn"], label=LBL["legit_learned"]) op = [(l, v) for l, v in zip(x, col(r, "oma")) if not math.isnan(v)] ax.semilogy([p[0] for p in op], [p[1] for p in op], **STY["oma"], label=LBL["oma"]) - ax.semilogy(x, col(r, "eve_ser"), **STY["eve"], label=LBL["eve_key"]) + ax.semilogy(x, col(r, "eve_ser"), **STY["eve"], markevery=(0, 2), + label=LBL["eve_key"], **UNDER) + # the permutation key shares this physical layer, so its legitimate + # curve lies on the structured one and appears at every key length + # rather than only in the tables. Its outsider measures 0.99997 to + # 0.99999 and would lie on the outsider curve already drawn, in the + # same style as this one and with no legend entry of its own, so the + # caption says where it sits instead. + rp = load("sec_keylen_perm.csv") + assert min(float(r["eve_ser"]) for r in rp) > 0.999, "the permutation outsider left the random-guess level" + ax.semilogy(col(rp, "L", int), col(rp, "legit_ser"), **STY["perm"], + markevery=(1, 2), label=LBL["perm"], **OVER) ax.set_ylim(top=22.0) # headroom above the flat eavesdropper curve # an error rate cannot exceed one, and the room below the data holds # the legend, since every curve decays to the right @@ -409,7 +420,7 @@ def fig_jam(): # axis-spanning lines, so it cannot move the legend off this one, and # a reference drawn along the legend frame reads as part of the box. ax.axhline(nojam, color=C_OMA, ls=(0, (1, 3)), lw=0.9, zorder=0) - ax.set_ylim(6e-3, 1.4) + ax.set_ylim(2.5e-2, 1.4) # a log axis spanning little more than a decade prints minor labels # like 6x10^-1 that consume the left margin, so only the decades are # labelled @@ -478,20 +489,20 @@ def fig_real(): r = load("real_sec_ter.csv") x = col(r, "snr_db") fig, ax = plt.subplots() - # insider and outsider still nearly coincide and are layered; the - # legitimate and OMA curves are separate at this frame + # two pairs nearly coincide here, the two legitimate realizations + # within a fifth of each other and the two adversaries both at the + # top, so each pair is layered and its markers staggered ax.semilogy(x, col(r, "ter_legit"), **STY["km_str"], - markevery=(0, 2), label=LBL["legit"], **UNDER) + markevery=(0, 3), label=LBL["legit"], **UNDER) rt = load("real_sec_ter_learned.csv") - ax.semilogy(col(rt, "snr_db"), col(rt, "ter_legit"), - **STY["km_lrn"], markevery=(1, 2), - label=LBL["legit_learned"]) + ax.semilogy(col(rt, "snr_db"), col(rt, "ter_legit"), **STY["km_lrn"], + markevery=(1, 3), label=LBL["legit_learned"], **OVER) ax.semilogy(x, col(r, "ter_oma"), **STY["oma"], - markevery=(1, 2), label=LBL["oma"], **OVER) + markevery=(2, 3), label=LBL["oma"]) ax.semilogy(x, col(r, "ter_insider"), **STY["insider"], - markevery=(0, 2), label=LBL["insider"], **UNDER) + markevery=(0, 3), label=LBL["insider"], **UNDER) ax.semilogy(x, col(r, "ter_eve"), **STY["eve"], - markevery=(1, 2), label=LBL["outsider"], **OVER) + markevery=(2, 3), label=LBL["outsider"], **OVER) ax.set_xlabel("SNR (dB)") ax.set_ylabel("TER") ax.set_xlim(min(x), max(x)) diff --git a/code/report_learned.py b/code/report_learned.py new file mode 100644 index 0000000..e8d01a1 --- /dev/null +++ b/code/report_learned.py @@ -0,0 +1,106 @@ +# -*- coding: utf-8 -*- +"""Every learned-family number the manuscript quotes, read from data/. + +Switching the learned family from the unpenalized keys to the +regularized ones of Section V-C moves every learned value in the paper. +This prints them next to their structured counterparts so the sentences +that carry them can be updated from one place, and so a later rerun can +be checked against what is printed. + +Run: python code/report_learned.py +""" +from __future__ import annotations + +import csv +import json +from pathlib import Path + +DATA = Path(__file__).resolve().parents[1] / "data" + + +def rows(name): + with open(DATA / name) as f: + return list(csv.DictReader(f)) + + +def at(rs, key, val, col): + for r in rs: + if abs(float(r[key]) - val) < 1e-9: + return float(r[col]) + raise KeyError("%s=%s not in the sweep" % (key, val)) + + +def main(): + print("== Fig. 2, SER against SNR ==") + s = rows("sec_snr.csv") + l = rows("sec_snr_learned.csv") + ratios = [] + for a, b in zip(s, l): + r = float(b["legit"]) / float(a["legit"]) + ratios.append(r) + print(" %5s dB str %.5f lrn %.5f ratio %.3f" + % (a["snr_db"], float(a["legit"]), float(b["legit"]), r)) + print(" ratio range %.3f to %.3f" % (min(ratios), max(ratios))) + + print("\n== Fig. 3, SER against key length ==") + s = rows("sec_keylen.csv") + l = rows("sec_keylen_learned.csv") + for a, b in zip(s, l): + print(" L=%-4s str %.5f lrn %.5f ratio %.3f kappa %.5f" + % (a["L"], float(a["legit_ser"]), float(b["legit_ser"]), + float(b["legit_ser"]) / float(a["legit_ser"]), + float(b["mask_xcorr"]))) + print(" learned outsider floor %.5f" + % min(float(r["eve_ser"]) for r in l)) + + print("\n== Fig. 4, jamming at JSR 0 dB ==") + print(" str blind %.4f" % at(rows("sec_jam.csv"), "jsr_db", 0.0, + "blind")) + print(" lrn blind %.4f" % at(rows("sec_jam_learned.csv"), "jsr_db", + 0.0, "blind")) + + print("\n== Fig. 6, best of K=1e6 guesses ==") + print(" str %.4f" % at(rows("sec_brute_cmp.csv"), "K", 1e6, "ser_mask")) + print(" lrn %.4f" % at(rows("sec_brute_learned.csv"), "K", 1e6, + "ser_mask")) + + print("\n== Fig. 7, known plaintext at 10 dB ==") + for name in ("kpa.csv", "kpa_learned.csv"): + r = [x for x in rows(name) if float(x["snr_db"]) == 10.0] + print(" %-16s N=2 %.4f N=8 %.4f N=64 %.4f" + % (name, at(r, "n_frames", 2, "eve_ser"), + at(r, "n_frames", 8, "eve_ser"), + at(r, "n_frames", 64, "eve_ser"))) + + print("\n== Fig. 8, real token streams ==") + s = rows("real_sec_ter.csv") + l = rows("real_sec_ter_learned.csv") + gaps = [] + for a, b in zip(s, l): + for col in ("ter_eve", "ter_insider"): + gaps.append(abs(float(a[col]) - float(b[col]))) + print(" %4s dB legit str %.5f lrn %.5f ratio %.3f" + % (a["snr_db"], float(a["ter_legit"]), float(b["ter_legit"]), + float(b["ter_legit"]) / float(a["ter_legit"]))) + print(" largest adversary gap between families %.2e" % max(gaps)) + for name in ("real_sec_stats.json", "real_sec_stats_learned.json"): + if (DATA / name).exists(): + d = json.loads((DATA / name).read_text()) + print(" %-28s %s" % (name, {k: d[k] for k in list(d)[:6]})) + + print("\n== Table IV, scheme comparison ==") + for name in ("sec_compare.csv", "compare_learned.csv"): + for r in rows(name): + print(" %-20s %s" % (name, dict(r))) + + print("\n== Table V, refresh ==") + l = rows("refresh_learned.csv") + print(" lrn legit %.5f to %.5f" + % (min(float(r["legit_ser"]) for r in l), + max(float(r["legit_ser"]) for r in l))) + print(" lrn eve %.5f" % (sum(float(r["eve_ser"]) for r in l) + / len(l))) + + +if __name__ == "__main__": + main() diff --git a/code/run_audit_fixes.py b/code/run_audit_fixes.py new file mode 100644 index 0000000..b8b204d --- /dev/null +++ b/code/run_audit_fixes.py @@ -0,0 +1,21 @@ +# -*- coding: utf-8 -*- +"""Rerun the four measurements the audit found mis-specified. + +refresh the learned rows ran 8 blocks while the table says 24 +jamming the learned curve ran a 5 dB grid inside a 2 dB figure +compare the learned row averaged four users inside a user-1 table +enum the enumeration attacks ran on the unpenalized learned keys +""" +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import exp_learned as E +import check_family_enum as F + +if __name__ == "__main__": + for fn in (E.refresh, E.jamming, E.compare, F.run): + print("=" * 60) + print("stage", fn.__module__ + "." + fn.__name__, flush=True) + fn() + print("[done] audit reruns complete") diff --git a/code/run_learned_reg.py b/code/run_learned_reg.py new file mode 100644 index 0000000..97caa14 --- /dev/null +++ b/code/run_learned_reg.py @@ -0,0 +1,28 @@ +# -*- coding: utf-8 -*- +"""Regenerate every learned-key artifact under the regularized loss. + +learned_model now trains under the two penalties of Section V-C, so +every *_learned file has to be rebuilt from it. sens runs before brute +because brute re-reads the sensitivity sweep it produced. +""" +import sys +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).resolve().parent)) +import exp_learned as E +import exp_full as F + +# stage_N is Fig. 2's learned SNR sweep and lives in exp_full, so it is +# named here rather than in exp_learned's own list +STAGES = [F.stage_N, E.keylen, E.jamming, E.kpa, E.refresh, E.compare, + E.sens, E.brute, E.real] + +if __name__ == "__main__": + only = sys.argv[1:] + for fn in STAGES: + if only and fn.__name__ not in only: + continue + print("=" * 60) + print("stage", fn.__name__, flush=True) + fn() + print("[done] every learned artifact rebuilt") diff --git a/data/compare_learned.csv b/data/compare_learned.csv index edc5d8a..1b3c583 100644 --- a/data/compare_learned.csv +++ b/data/compare_learned.csv @@ -1,2 +1,2 @@ scheme,legit_ser,eve_out,eve_in,jam0_ser -proposed_learned,0.06358416667,0.9998083333,0.9999833333,0.4046066667 +proposed_learned,0.06102,0.999,0.99998,0.3971366667 diff --git a/data/jscc.csv b/data/jscc.csv new file mode 100644 index 0000000..ee24e8e --- /dev/null +++ b/data/jscc.csv @@ -0,0 +1,20 @@ +part,case,structured_ser,structured_sd,learned_ser,kappa_and_verdict +A one user,Walsh row,0.01330,0.00007,, +A one user,all ones (no mask),0.01325,0.00022,, +A one user,"learned, free",0.01480,0.00045,, +B key length,L=6,0.99610,0.00048,0.99722,0.11111/tie +B key length,L=8,0.94056,0.00686,0.95188,0.00000/tie +B key length,L=10,0.83318,0.03634,0.80853,0.06667/tie +B key length,L=12,0.41391,0.00154,0.41663,0.00000/tie +B key length,L=14,0.39963,0.00779,0.37367,0.04762/learned +B key length,L=16,0.25810,0.00119,0.28998,0.00000/structured +B key length,L=18,0.23796,0.00182,0.23525,0.03704/tie +B key length,L=20,0.18811,0.00113,0.21565,0.00000/structured +B key length,L=22,0.17300,0.00072,0.18993,0.03030/structured +B key length,L=24,0.15290,0.00034,0.17582,0.00000/structured +C load,U=4,0.25810,0.00119,0.28998,0.00000/structured +C load,U=8,0.95022,0.01381,0.97322,0.00000/tie +C load,U=12,0.99882,0.00005,0.99867,0.00000/tie +C load,U=15,0.99976,0.00000,0.99976,0.00000/tie +C load,U=16,0.99976,0.00001,0.99976,0.00000/tie +C load,U=20,0.99982,0.00000,0.99981,0.02105/learned diff --git a/data/kpa_learned.csv b/data/kpa_learned.csv index 6bf8c12..e1e896d 100644 --- a/data/kpa_learned.csv +++ b/data/kpa_learned.csv @@ -1,43 +1,43 @@ snr_db,n_frames,kappa,eve_ser -0,1,0.2127109103,0.993336 -0,2,0.7559393242,0.666508 -0,3,0.8627683729,0.392272125 -0,4,0.9179756209,0.218212875 -0,5,0.945390512,0.14425925 -0,6,0.9590921029,0.107493 -0,8,0.9725584686,0.086112625 -0,10,0.9778045967,0.07933375 -0,12,0.9831736788,0.07420325 -0,16,0.9879393309,0.070712875 -0,24,0.9925390184,0.067800375 -0,32,0.9945808738,0.06649325 -0,48,0.9965663388,0.065265 -0,64,0.9975094497,0.065024875 -10,1,0.3165432975,0.948993875 -10,2,0.9290210679,0.19776825 -10,3,0.9781143948,0.095876875 -10,4,0.9896475986,0.070495125 -10,5,0.9934410676,0.067297125 -10,6,0.9953705788,0.06628575 -10,8,0.9971551418,0.06520175 -10,10,0.9979188025,0.064684875 -10,12,0.9982561454,0.06454375 -10,16,0.9987509355,0.06426625 -10,24,0.9992754847,0.064091125 -10,32,0.9994516179,0.06386675 -10,48,0.9996520028,0.063819875 -10,64,0.999746412,0.063847125 -20,1,0.742194891,0.513137625 -20,2,0.9947786465,0.06753025 -20,3,0.9986294076,0.06438075 -20,4,0.9992210969,0.064160375 -20,5,0.9994008377,0.064080125 -20,6,0.9995701849,0.063900375 -20,8,0.9997365534,0.063852875 -20,10,0.9998067141,0.063813 -20,12,0.9998438716,0.06370225 -20,16,0.9998808399,0.063670875 -20,24,0.9999239221,0.06380125 -20,32,0.9999452353,0.063820625 -20,48,0.9999649763,0.063811 -20,64,0.9999733046,0.06377525 +0,1,0.05034990469,0.997911625 +0,2,0.05894429332,0.662401 +0,3,0.08379636761,0.37693875 +0,4,0.107222241,0.22233425 +0,5,0.1292011227,0.152770125 +0,6,0.1429406652,0.131741625 +0,8,0.1788643047,0.086013875 +0,10,0.2027079877,0.077310875 +0,12,0.2198284302,0.07391475 +0,16,0.2579282403,0.069537375 +0,24,0.3094305053,0.066363625 +0,32,0.3459532984,0.06496575 +0,48,0.4114221439,0.063700375 +0,64,0.4645803586,0.06309375 +10,1,0.05442041112,0.950243625 +10,2,0.1407177548,0.181911125 +10,3,0.2204171771,0.080573875 +10,4,0.2837046772,0.072395125 +10,5,0.3326446027,0.06649075 +10,6,0.3704789877,0.0648705 +10,8,0.4370021872,0.063752375 +10,10,0.479882317,0.062964 +10,12,0.5199262805,0.062786125 +10,16,0.5935128644,0.062229125 +10,24,0.6796541184,0.061876875 +10,32,0.7379264548,0.0618305 +10,48,0.8053073436,0.061516125 +10,64,0.8465657607,0.061403 +20,1,0.06401134632,0.757281 +20,2,0.3653038088,0.06925225 +20,3,0.5262307867,0.06323275 +20,4,0.6446151808,0.06203725 +20,5,0.7232914954,0.06185625 +20,6,0.7802997425,0.06165275 +20,8,0.8357323289,0.061493375 +20,10,0.8705290645,0.061302375 +20,12,0.8942843586,0.061368125 +20,16,0.9240833685,0.06133 +20,24,0.9489109725,0.061323375 +20,32,0.9614683628,0.06120425 +20,48,0.97474062,0.061322875 +20,64,0.9812480465,0.06146825 diff --git a/data/real_sec_stats_learned.json b/data/real_sec_stats_learned.json index 37d3600..403ba01 100644 --- a/data/real_sec_stats_learned.json +++ b/data/real_sec_stats_learned.json @@ -11,19 +11,19 @@ "headline_runs": 4, "recovery": { "20": { - "legit": 0.7023870636550308, + "legit": 0.7183008213552361, "eve": 0.0, "insider": 0.0, "oma": 0.6463039014373717 }, "24": { - "legit": 0.8787217659137577, + "legit": 0.8805184804928131, "eve": 0.0, "insider": 0.0, "oma": 0.8390657084188912 }, "28": { - "legit": 0.9477669404517454, + "legit": 0.946611909650924, "eve": 0.0, "insider": 0.0, "oma": 0.9319815195071869 diff --git a/data/real_sec_ter_learned.csv b/data/real_sec_ter_learned.csv index f93cf50..8ab9dbe 100644 --- a/data/real_sec_ter_learned.csv +++ b/data/real_sec_ter_learned.csv @@ -1,9 +1,9 @@ snr_db,ter_legit,ter_eve,ter_insider,ter_oma -0,0.4516948856,0.9998824906,0.9962722018,0.5232856128 -4,0.223975418,0.9998662393,0.9947945836,0.2740881771 -8,0.09790408233,0.9997899832,0.9940332727,0.123424874 -12,0.04099952996,0.9997912333,0.9936357409,0.05201666133 -16,0.01653382271,0.9997299784,0.9935107309,0.02118419474 -20,0.006725538043,0.999737479,0.9934594768,0.008630690455 -24,0.002707716617,0.9997112269,0.9934182235,0.003452776222 -28,0.001037583007,0.9997274782,0.9934144732,0.001385110809 +0,0.4424778982,0.9998887411,0.9961971958,0.5232856128 +4,0.2185937375,0.9998887411,0.9948020842,0.2740881771 +8,0.09510385831,0.9998837407,0.9940082707,0.123424874 +12,0.03968067445,0.9998599888,0.9936469918,0.05201666133 +16,0.0160362829,0.999899992,0.9935182315,0.02118419474 +20,0.006499269942,0.999887491,0.9934544764,0.008630690455 +24,0.002587707017,0.9998849908,0.9934232239,0.003452776222 +28,0.001065085207,0.999887491,0.9934157233,0.001385110809 diff --git a/data/refresh_learned.csv b/data/refresh_learned.csv index 4efc8c4..fe814f3 100644 --- a/data/refresh_learned.csv +++ b/data/refresh_learned.csv @@ -1,9 +1,25 @@ block,legit_ser,eve_ser -0,0.06381666667,0.9999375 -1,0.06395583333,0.9981941667 -2,0.06397833333,0.999985 -3,0.06386833333,0.9983758333 -4,0.06338666667,0.9999908333 -5,0.06366416667,0.9999133333 -6,0.06398416667,0.9999433333 -7,0.06390583333,0.9809091667 +0,0.06113666667,0.9990225 +1,0.06151666667,0.99906 +2,0.06108666667,0.9999341667 +3,0.0615975,0.9995841667 +4,0.06189833333,0.9996233333 +5,0.0608925,0.9997391667 +6,0.06110333333,0.9999591667 +7,0.06100083333,0.994235 +8,0.06136166667,0.9995625 +9,0.06109166667,0.999875 +10,0.06130416667,0.9996441667 +11,0.06130916667,0.9975941667 +12,0.06105666667,0.9999325 +13,0.06113166667,0.9998916667 +14,0.06178,0.9994783333 +15,0.06124583333,0.9997775 +16,0.06141083333,0.9998683333 +17,0.0612175,0.99964 +18,0.06126833333,0.9997041667 +19,0.06113916667,0.9999766667 +20,0.06117583333,0.9994191667 +21,0.06127166667,0.9999558333 +22,0.06118416667,0.9995833333 +23,0.06149666667,0.9997783333 diff --git a/data/refresh_summary.csv b/data/refresh_summary.csv index 859be91..05d8cb3 100644 --- a/data/refresh_summary.csv +++ b/data/refresh_summary.csv @@ -2,4 +2,4 @@ scheme,legit,eve,entropy_bits None (fixed key),0.05300666667,0.9997075,23.76910417 Fresh orthogonal keys,0.1215548611,0.9996535069,23.76910417 "Invariant, KM (str.)",0.05304121528,0.9995528819,364.5801064 -"Invariant, KM (lrn.)",0.063800,0.997200,364.5801064 +"Invariant, KM (lrn.)",0.061279,0.998895,364.5801064 diff --git a/data/sec_brute_learned.csv b/data/sec_brute_learned.csv index 88125cd..6e501c8 100644 --- a/data/sec_brute_learned.csv +++ b/data/sec_brute_learned.csv @@ -1,15 +1,15 @@ K,ser_mask -1,0.9967928683 -3,0.9934662748 -10,0.9869523182 -30,0.9769415244 -100,0.961966217 -300,0.9457608404 -1000,0.9257560018 -3000,0.8881725568 -10000,0.8471349462 -30000,0.8132026814 -65536,0.790006818 -100000,0.7797171991 -300000,0.7435325695 -1000000,0.7188559867 +1,0.9967152519 +3,0.9933139604 +10,0.9866396597 +30,0.976371298 +100,0.9609886779 +300,0.9443205866 +1000,0.92371647 +3000,0.8849669152 +10000,0.8426466786 +30000,0.8076524286 +65536,0.7837337919 +100000,0.7731184695 +300000,0.7358238662 +1000000,0.7103847689 diff --git a/data/sec_compare.csv b/data/sec_compare.csv index 0336c0a..2fdbaf8 100644 --- a/data/sec_compare.csv +++ b/data/sec_compare.csv @@ -4,4 +4,4 @@ public_mask,0.0529425,0.0529425,0.0529425,0.83882 perm_key,0.0528525,0.9999925,0.0528525,0.36425 index_cipher,0.0529425,0.9999847412,0.9999847412,0.83882 oma_plain,0.08056383667,0.08056383667,0.08056383667,nan -proposed_learned,0.06358416667,0.9998083333,0.9999833333,0.4046066667 +proposed_learned,0.06156083333,0.9996875,0.9999841667,0.3973533333 diff --git a/data/sec_jam_learned.csv b/data/sec_jam_learned.csv index efd1cac..350bb0f 100644 --- a/data/sec_jam_learned.csv +++ b/data/sec_jam_learned.csv @@ -1,8 +1,17 @@ jsr_db,blind,matched,nojam --10,0.117582,0.40832,0.062852 --5,0.215158,0.657572,0.062852 -0,0.404244,0.851282,0.062852 -5,0.648786,0.94662,0.062852 -10,0.839218,0.982652,0.062852 -15,0.939304,0.994184,0.062852 -20,0.979206,0.99818,0.062852 +-10,0.114422,0.38569,0.060494 +-8,0.143006,0.483718,0.060494 +-6,0.183328,0.586128,0.060494 +-4,0.239698,0.685262,0.060494 +-2,0.310968,0.772466,0.060494 +0,0.398118,0.840574,0.060494 +2,0.493594,0.893422,0.060494 +4,0.594688,0.929018,0.060494 +6,0.686344,0.953816,0.060494 +8,0.768698,0.970236,0.060494 +10,0.835132,0.980904,0.060494 +12,0.886322,0.98835,0.060494 +14,0.923646,0.992284,0.060494 +16,0.949318,0.995096,0.060494 +18,0.967192,0.996858,0.060494 +20,0.978764,0.998038,0.060494 diff --git a/data/sec_keylen_learned.csv b/data/sec_keylen_learned.csv index 4c1275d..5adaff5 100644 --- a/data/sec_keylen_learned.csv +++ b/data/sec_keylen_learned.csv @@ -1,9 +1,9 @@ L,d,legit_ser,eve_ser,mask_xcorr,oma -8,32,0.9297855,0.9998735,0.007307400461,0.6849191155 -12,48,0.416604,0.9997065,0.005153660662,nan -16,64,0.2762895,0.999912,0.007116591092,0.2747696909 -20,80,0.2076175,0.999383,0.003162040841,0.2289444229 -24,96,0.1829615,0.999894,0.002973971656,0.1961714033 -32,128,0.131901,0.999616,0.005575809628,0.1524639978 -48,192,0.090206,0.9994575,0.005743456539,0.1054308944 -64,256,0.0635265,0.999637,0.006678360514,0.08056383667 +8,32,0.9594105,0.99997,0.003249221947,0.6849191155 +12,48,0.4157085,0.999767,0.003225991037,nan +16,64,0.315952,0.9994685,0.01121100038,0.2747696909 +20,80,0.213059,0.9996415,0.00134725438,0.2289444229 +24,96,0.183031,0.9997345,0.001526024193,0.1961714033 +32,128,0.1231225,0.999805,0.002514706925,0.1524639978 +48,192,0.084432,0.999786,0.001822981867,0.1054308944 +64,256,0.061412,0.9993955,0.002381352475,0.08056383667 diff --git a/data/sec_keylen_perm.csv b/data/sec_keylen_perm.csv new file mode 100644 index 0000000..da4d00e --- /dev/null +++ b/data/sec_keylen_perm.csv @@ -0,0 +1,9 @@ +L,d,legit_ser,eve_ser +8,32,0.948556,0.999984 +12,48,0.414228,0.99995 +16,64,0.258384,0.999992 +20,80,0.187222,0.999986 +24,96,0.152004,0.999972 +32,128,0.10816,0.999976 +48,192,0.071734,0.999966 +64,256,0.053092,0.999976 diff --git a/data/sec_sens_learned.csv b/data/sec_sens_learned.csv index 2260e94..4a25277 100644 --- a/data/sec_sens_learned.csv +++ b/data/sec_sens_learned.csv @@ -1,14 +1,14 @@ frac,ser_mask -0,0.9999754167 -0.2,0.9977270833 -0.4,0.9502445833 -0.6,0.6829479167 -0.75,0.31458375 -0.85,0.1250175 -0.9,0.09168625 -0.92,0.08508416667 -0.94,0.07654291667 -0.955,0.0716375 -0.97,0.06938458333 -0.985,0.06607833333 -1,0.0636775 +0,0.9999529167 +0.2,0.99770125 +0.4,0.9489816667 +0.6,0.6733195833 +0.75,0.29680625 +0.85,0.1140279167 +0.9,0.08714708333 +0.92,0.07980791667 +0.94,0.07278666667 +0.955,0.06856208333 +0.97,0.06626333333 +0.985,0.06358166667 +1,0.06154625 diff --git a/data/sec_snr_learned.csv b/data/sec_snr_learned.csv index 29272d8..9ed51b5 100644 --- a/data/sec_snr_learned.csv +++ b/data/sec_snr_learned.csv @@ -1,12 +1,12 @@ snr_db,legit,eve_wrong -0,0.45205625,0.999865625 -2,0.325615625,0.9998425 -4,0.224483125,0.9998196875 -6,0.150013125,0.999801875 -8,0.0982703125,0.9997996875 -10,0.06379375,0.9997821875 -12,0.0409540625,0.999758125 -14,0.0259790625,0.9997684375 -16,0.0165590625,0.999769375 -18,0.01049375,0.999760625 -20,0.006573125,0.9997428125 +0,0.439821875,0.999816875 +2,0.31516125,0.9997828125 +4,0.217330625,0.9997790625 +6,0.1449359375,0.9997196875 +8,0.0947978125,0.99971375 +10,0.0611025,0.999680625 +12,0.039268125,0.9996478125 +14,0.0251025,0.999646875 +16,0.0159153125,0.999625625 +18,0.0100140625,0.9996171875 +20,0.0063884375,0.999608125 diff --git a/data/verify_math.csv b/data/verify_math.csv index 7686a7d..da61c1d 100644 --- a/data/verify_math.csv +++ b/data/verify_math.csv @@ -15,4 +15,4 @@ V7 symbolic identities,exact,exact,0,0,PASS V8 cross-period remainder,0.0,0.000337,0.000337,0.0005,PASS V9 score-variance ratio,2.8,2.8252,0.0252,0.05,PASS V10 format-matched OMA at 10 dB,0.055,0.05520,0.00020,0.001,PASS -V11 OMA closed form vs Monte Carlo,0.081118,0.080925,0.0024,0.01,PASS +V11 OMA closed form vs Monte Carlo,0.081245,0.080925,0.0039,0.01,PASS diff --git a/data/whygap.csv b/data/whygap.csv new file mode 100644 index 0000000..eae3a82 --- /dev/null +++ b/data/whygap.csv @@ -0,0 +1,4 @@ +family,legit_ser,kappa_bar,entry_use +structured (frozen),0.05259,0.00000,1.0000 +learned (free start),0.06363,0.00444,0.0916 +learned (Walsh start),0.06489,0.00478,0.2797 diff --git a/fig/fig_sec_brute.pdf b/fig/fig_sec_brute.pdf index 895afa9..6c705d7 100644 Binary files a/fig/fig_sec_brute.pdf and b/fig/fig_sec_brute.pdf differ diff --git a/fig/fig_sec_jam.pdf b/fig/fig_sec_jam.pdf index 6b301cf..4e89528 100644 Binary files a/fig/fig_sec_jam.pdf and b/fig/fig_sec_jam.pdf differ diff --git a/fig/fig_sec_keylen.pdf b/fig/fig_sec_keylen.pdf index 540a5da..da4a578 100644 Binary files a/fig/fig_sec_keylen.pdf and b/fig/fig_sec_keylen.pdf differ diff --git a/fig/fig_sec_kpa.pdf b/fig/fig_sec_kpa.pdf index 6c3bef2..ee0e685 100644 Binary files a/fig/fig_sec_kpa.pdf and b/fig/fig_sec_kpa.pdf differ diff --git a/fig/fig_sec_real.pdf b/fig/fig_sec_real.pdf index 5de6191..e7bd0bf 100644 Binary files a/fig/fig_sec_real.pdf and b/fig/fig_sec_real.pdf differ diff --git a/fig/fig_sec_sens.pdf b/fig/fig_sec_sens.pdf index d9599bb..4ef3499 100644 Binary files a/fig/fig_sec_sens.pdf and b/fig/fig_sec_sens.pdf differ diff --git a/fig/fig_sec_snr.pdf b/fig/fig_sec_snr.pdf index a67853a..43aa662 100644 Binary files a/fig/fig_sec_snr.pdf and b/fig/fig_sec_snr.pdf differ