diff --git a/README.md b/README.md index 761be92..49432f7 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,10 @@ code/ schemes, key families, scheme comparison, attack difficulty exp_kpa.py stage H: known-plaintext attack on the key exp_refresh.py stage K: the key-refresh layer, invariance group + exp_permkpa.py permutation-key known-plaintext attack (Fig. 7) + 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-V5 against Monte Carlo, PASS/FAIL + verify_math.py closed-form checks V1-V5, PASS/FAIL and verify_math.csv 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 @@ -66,10 +68,10 @@ files. |---|---|---| | 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 | `exp_full.stage_L` | `sec_jam_cmp.csv`, `sec_jam.csv` | +| Fig. 4 jamming (4 schemes) | `exp_full.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_J` | `sec_brute_cmp.csv`, `sec_brute.csv` | -| Fig. 7 known-plaintext attack | `exp_kpa` | `kpa.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` | diff --git a/code/check_cov_attack.py b/code/check_cov_attack.py new file mode 100644 index 0000000..accd361 --- /dev/null +++ b/code/check_cov_attack.py @@ -0,0 +1,112 @@ +"""Independent check of the ciphertext-only second-order attack (audit M1). + +Claim under test: an eavesdropper who observes only received frames (no +known indices) can estimate the key Gram matrix M^T M from the sample +covariance, because per period + E[y_k y_l] = (1/c^2) * E[h^2] * C_kl * (M^T M)_kl, +where C_kl = (1/Vu) sum_i b_{i,k} b_{i,l} is the PUBLIC codebook column +correlation and the noise touches only the diagonal. + +Procedure, using nothing the threat model keeps secret: + 1. collect N received frames y_n = h_n * (1/c) sum_u e_{s_u} ⊙ m_u + noise + 2. form the per-period sample second moment S_kl = mean_n y_{n,k} y_{n,l} + 3. divide the off-diagonal by C_kl (public) to get G_hat ≈ M^T M + 4. set the diagonal of G_hat to U (unit-modulus keys) + 5. factor G_hat = M_hat^T M_hat (rank U), then for Walsh-Hadamard keys + round to ±1 and search the 2^U U! signed permutations, keeping the + M_hat that best decodes a handful of the collected frames + 6. report the recovered-entry fraction and the eavesdropper SER, both + WITHOUT ever using a known index + +Run under WSL. Prints a verdict; writes nothing to data/. +""" +from __future__ import annotations +import itertools +import math +import numpy as np +import torch + +from sse_lib import rayleigh_gain, DEVICE +from exp_full import get_model, hadamard, eval_ser_eve + + +def collect_frames(m, n, snr_db, seed): + """Received frames and the true indices (indices kept only for scoring).""" + g = torch.Generator().manual_seed(seed) + Bn = m.unit_codebook() + true_m = m.masks() + c = m.c + sigma = math.sqrt(1.0 / (m.d * 10.0 ** (snr_db / 10.0))) + digits = torch.randint(m.vu, (n, m.users, m.P), generator=g).to(DEVICE) + e = Bn[digits] / math.sqrt(m.P) # (n,U,P,L) + y = (e * true_m[None, :, None, :]).sum(dim=1) / c # (n,P,L) + h = rayleigh_gain((n,), device=DEVICE) + y = h[:, None, None] * y + sigma * torch.randn(n, m.P, m.L, device=DEVICE) + return y, digits, Bn, true_m + + +def codebook_corr(Bn): + """Public column correlation C_kl = (1/Vu) sum_i b_ik b_il.""" + return (Bn.T @ Bn) / Bn.shape[0] # (L,L) + + +def attack(m, snr_db, n_frames, seed): + y, digits, Bn, true_m = collect_frames(m, n_frames, snr_db, seed) + U, L = m.users, m.L + yf = y.reshape(-1, L) # pool all periods + S = (yf.T @ yf) / yf.shape[0] # (L,L) 2nd moment + C = codebook_corr(Bn) # public + G = torch.zeros(L, L, device=DEVICE) + mask = C.abs() > 1e-3 + G[mask] = S[mask] / C[mask] # ≈ (1/c^2) M^T M + scale = float(torch.diagonal(G)[mask.diagonal()].mean()) / U + G = G / max(scale, 1e-9) # normalize so diag≈U + G.fill_diagonal_(float(U)) # unit-modulus keys + # symmetric rank-U factor + G = 0.5 * (G + G.T) + evals, evecs = torch.linalg.eigh(G) + idx = torch.argsort(evals, descending=True)[:U] + root = evecs[:, idx] * evals[idx].clamp_min(0).sqrt() + Mhat0 = root.T # (U,L), up to U×U orth + # for WH keys, snap to ±1 and search signed row permutations + cand = torch.sign(Mhat0) + cand[cand == 0] = 1.0 + best = None + best_ser = 1.0 + val = torch.arange(min(2000, n_frames)) + for perm in itertools.permutations(range(U)): + for signs in itertools.product([1.0, -1.0], repeat=U): + Mh = (cand[list(perm)] * + torch.tensor(signs, device=DEVICE)[:, None]) + ser = eval_ser_eve(m, Mh.cpu(), [snr_db], frames=20_000, + seed=13)[0] + if ser < best_ser: + best_ser, best = ser, Mh + # recovered-entry fraction against the true keys (best sign-aligned) + tm = torch.sign(true_m).to(DEVICE) + frac = 0.0 + for perm in itertools.permutations(range(U)): + for signs in itertools.product([1.0, -1.0], repeat=U): + Mh = (best[list(perm)] * + torch.tensor(signs, device=DEVICE)[:, None]) + frac = max(frac, float((Mh == tm).float().mean())) + return frac, best_ser + + +def main(): + U, L = 4, 16 + K0 = torch.tensor(hadamard(L)[1:U + 1], dtype=torch.float32) + m = get_model(iters=4000, freeze_W=K0) + m.eval() + chance = 1.0 - (1.0 / m.vu) ** m.P + print(f"chance SER = {chance:.5f}, legitimate reference ~0.276") + print("ciphertext-only (NO known plaintext):") + for snr in (10.0, 20.0): + for nf in (300, 1000, 10000): + frac, ser = attack(m, snr, nf, seed=1234 + nf) + print(f" {snr:4.0f} dB N={nf:6d} " + f"key-entry recovery={frac:.3f} eve SER={ser:.4f}") + + +if __name__ == "__main__": + main() diff --git a/code/check_cov_ceiling.py b/code/check_cov_ceiling.py new file mode 100644 index 0000000..947272b --- /dev/null +++ b/code/check_cov_ceiling.py @@ -0,0 +1,82 @@ +"""Decisive noiseless population test of the covariance attack (audit M1). + +If the second-order statistics leak the key Gram, they leak it best in +the noiseless infinite-sample limit. This computes the EXACT per-period +second moment E[y_k y_l] over the uniform index distribution with no +channel and no noise, then runs the same recovery, and asks whether the +keys come out. If they do not come out even here, no finite noisy attack +can do better and the leak is not exploitable against this codebook. +""" +from __future__ import annotations +import itertools +import math +import numpy as np +import torch + +from sse_lib import DEVICE +from exp_full import get_model, hadamard, eval_ser_eve + + +def main(): + U, L = 4, 16 + K0 = torch.tensor(hadamard(L)[1:U + 1], dtype=torch.float32) + m = get_model(iters=4000, freeze_W=K0) + m.eval() + Bn = m.unit_codebook().to(DEVICE) # (Vu,L) + true_m = m.masks().to(DEVICE) # (U,L) + c = m.c + + # exact population second moment of one period, indices uniform + # y_k = (1/c) sum_u e_{s_u,k} m_{u,k}, s_u iid uniform over Vu + mu = Bn.mean(0) # codebook column mean + R = (Bn.T @ Bn) / Bn.shape[0] # E[e_k e_l], (L,L) + G_true = true_m.T @ true_m # (L,L) key Gram, the target + # E[y_k y_l] = (1/c^2)[ R_kl (M^TM)_kl + (mu_k mu_l)(rowsum_k rowsum_l + # - diag correction) ]; assemble exactly + rs = true_m.sum(0) # sum_u m_{u,k} + cross = torch.outer(rs, rs) - G_true # sum_{u!=v} m_uk m_vl + S = (R * G_true + torch.outer(mu, mu) * cross) / (c * c) + + print(f"codebook column mean |mu|_max = {mu.abs().max():.4f}") + offdiag = R - torch.diag(torch.diagonal(R)) + print(f"codebook R off-diagonal: max|R_kl| = {offdiag.abs().max():.4f}, " + f"mean|R_kl| = {offdiag.abs().mean():.4f}") + + # recover G from S using the public R (exactly the attack) + C = R + keep = C.abs() > 1e-2 + Ghat = torch.zeros(L, L, device=DEVICE) + Ghat[keep] = S[keep] * (c * c) / C[keep] + # how well does the off-diagonal of Ghat match the true key Gram? + od = ~torch.eye(L, dtype=torch.bool, device=DEVICE) + usable = keep & od + if usable.any(): + err = (Ghat[usable] - G_true[usable]).abs().mean() + rng = G_true[od].abs().mean() + print(f"usable off-diagonal entries: {int(usable.sum())} of {L*(L-1)}") + print(f"recovered-Gram error on usable entries: {err:.4f} " + f"(true off-diag scale {rng:.4f})") + else: + print("no usable off-diagonal entries: R is diagonal, zero leak") + + # try to factor and decode from the exact-population Ghat + Ghat[~keep] = 0.0 + Ghat.fill_diagonal_(float(U)) + Ghat = 0.5 * (Ghat + Ghat.T) + ev, evec = torch.linalg.eigh(Ghat) + idx = torch.argsort(ev, descending=True)[:U] + root = (evec[:, idx] * ev[idx].clamp_min(0).sqrt()).T + cand = torch.sign(root) + cand[cand == 0] = 1.0 + best = 1.0 + for perm in itertools.permutations(range(U)): + for sg in itertools.product([1.0, -1.0], repeat=U): + Mh = cand[list(perm)] * torch.tensor(sg, device=DEVICE)[:, None] + best = min(best, eval_ser_eve(m, Mh.cpu(), [10.0], + frames=20_000, seed=13)[0]) + print(f"best eavesdropper SER from EXACT population covariance: {best:.4f}") + print("chance 0.99998, legitimate ~0.276") + + +if __name__ == "__main__": + main() diff --git a/code/exp_full.py b/code/exp_full.py index cd0eb16..11a6c19 100644 --- a/code/exp_full.py +++ b/code/exp_full.py @@ -152,7 +152,7 @@ def get_model(P=4, vu=16, d=64, U=4, iters=4000, seed=1, freeze_W=None, tag=""): def stage_A(): print("[A] security vs SNR (V=65536) ...") m = get_model(iters=4000) - snr = [0.0, 4.0, 8.0, 12.0, 16.0, 20.0] + snr = [float(v) for v in range(0, 21, 2)] frames = 800_000 legit = eval_ser_sse(m, snr, frames=frames) ew = eve_wrong_mask(m.users, m.L, seed=20260813).to(DEVICE) @@ -209,18 +209,46 @@ def get_model_reg(P=4, vu=16, d=64, U=4, iters=4000, seed=1): return m +def oma_ser_keylen(L, snr_db, bits=16, n_grid=200_000): + """Resource-matched OMA reference for the key-length sweep. + + The OMA user owns d/U = L exclusive real dimensions for its 16 index + bits at the same per-dimension SNR. For L >= 16 the best use of the + allocation is antipodal signaling on 16 dimensions with the frame + energy concentrated on them, an energy gain of L/16 per bit. For + L < 16 the user must pack 16/L bits per dimension, a 2^(16/L)-ary + pulse-amplitude constellation, defined when 16/L is an integer and + reported as nan otherwise. + """ + import numpy as np + if L >= bits: + return oma_ser([snr_db + 10.0 * math.log10(L / bits)], bits=bits)[0] + if bits % L: + return float("nan") + M = 2 ** (bits // L) + x = (np.arange(n_grid) + 0.5) / n_grid + h = np.sqrt(-np.log(1.0 - x)) + g = 10.0 ** (snr_db / 10.0) + arg = np.clip(h * math.sqrt(6.0 * g / (M * M - 1.0)), 0, 38) + q = (1.0 - 1.0 / M) * np.array([math.erfc(v / math.sqrt(2.0)) + for v in arg]) + q = np.clip(q, 0.0, 1.0) + return float(np.mean(1.0 - (1.0 - q) ** L)) + + def stage_B(): print("[B] key length (dense grid so the curve is smooth) ...") - oma10 = oma_ser([10.0], bits=16)[0] rows = [] - for d in [16, 24, 32, 48, 64, 96, 128, 192, 256]: + for d in [16, 24, 32, 40, 48, 56, 64, 80, 96, 128, 192, 256]: m = get_model(d=d, iters=4000) lg = eval_ser_sse(m, [10.0], frames=500_000)[0] ew = eve_wrong_mask(m.users, m.L, seed=20260813).to(DEVICE) ev = eval_ser_eve(m, ew, [10.0], frames=500_000)[0] xc = mean_abs_xcorr(m.masks().detach()) - rows.append((m.L, d, lg, ev, xc, oma10)) - print(f" L={m.L:4d} legit={lg:.2e} eve={ev:.3f} xcorr={xc:.4f}") + oma = oma_ser_keylen(m.L, 10.0) + rows.append((m.L, d, lg, ev, xc, oma)) + print(f" L={m.L:4d} legit={lg:.2e} eve={ev:.3f} xcorr={xc:.4f} " + f"oma={oma:.4f}") write_csv(DATA / "sec_keylen.csv", ["L", "d", "legit_ser", "eve_ser", "mask_xcorr", "oma"], rows) @@ -548,8 +576,10 @@ def stage_I(): between the guessed and the true mask. For the permutation scheme it is the fraction of positions the guessed permutation places correctly. For the index cipher it is the fraction of pad bits the - attacker knows, whose error rate is the closed form - 1 - 2^{-(1-f) log2 V} because the unknown bits are uniform. + attacker knows. Its error rate is the closed form + 1 - (1-p_ch) 2^{-(1-f) log2 V}, the probability of decoding the + ciphered index over the channel times the probability that the + unknown pad bits, which stay uniform, are all guessed right. """ print("[I] key sensitivity across schemes ...") m = get_model(iters=4000) @@ -563,8 +593,12 @@ def stage_I(): perms = gperm[None].repeat(m.users, 1) # a marker grid comparable to the other result figures, with the # spacing tightened only where the curves fall - fracs = [0.0, 0.2, 0.4, 0.6, 0.75, 0.85, 0.9, 0.94, 0.97, 1.0] + fracs = [0.0, 0.2, 0.4, 0.6, 0.75, 0.85, 0.9, 0.92, 0.94, 0.955, + 0.97, 0.985, 1.0] bits = math.log2(m.V) + # the channel success of a public-mask receiver, which the cipher + # cannot exceed even with the full pad + lg1 = eval_scheme(m, 10.0, 200_000) rows = [] for f in fracs: acc_m = [] @@ -585,7 +619,7 @@ def stage_I(): perms, eve_perms=pperms, seed=777 + 13 * t)) ser_perm = sum(acc) / len(acc) - ser_pad = 1.0 - 2.0 ** (-(1.0 - f) * bits) + ser_pad = 1.0 - (1.0 - lg1) * 2.0 ** (-(1.0 - f) * bits) rows.append((f, ser_mask, ser_perm, ser_pad)) print(f" f={f:.3f} mask={ser_mask:.4f} perm={ser_perm:.4f} " f"pad={ser_pad:.4f}") @@ -602,7 +636,8 @@ def stage_J(): one that places the most positions correctly, map the resulting fraction through the same sensitivity curve. Index cipher: K random pads out of the 2^{log2 V} possible pads, so - the attacker succeeds with probability K/V on each symbol. + the attacker holds the right pad with probability K/V and still has + to decode the ciphered index over the channel. """ print("[J] brute-force search across schemes ...") import numpy as np @@ -612,7 +647,11 @@ def stage_J(): perm_arr = np.array([float(r["ser_perm"]) for r in cmp_rows]) d, L, V = 64, 16, 65536 - ks = [1, 10, 100, 1_000, 10_000, 100_000, 1_000_000] + # channel floor of the cipher receiver, read from the stage-I curve + # at a fully known pad so both figures share one source + lg1 = 1.0 - (1.0 - float(cmp_rows[-1]["ser_pad"])) + ks = [1, 3, 10, 30, 100, 300, 1_000, 3_000, 10_000, 30_000, 65_536, + 100_000, 300_000, 1_000_000] rng = np.random.default_rng(2026) trials = 400 rows = [] @@ -629,7 +668,7 @@ def stage_J(): best_frac[t] = rng.binomial(d, 1.0 / d, size=K).max() / d ser_mask = float(np.mean(np.interp(best_kappa, f_arr, mask_arr))) ser_perm = float(np.mean(np.interp(best_frac, f_arr, perm_arr))) - ser_pad = 1.0 - min(1.0, K / V) + ser_pad = 1.0 - min(1.0, K / V) * (1.0 - lg1) rows.append((K, ser_mask, ser_perm, ser_pad, float(best_kappa.mean()), float(best_frac.mean()))) print(f" K={K:8d} mask={ser_mask:.4f} perm={ser_perm:.4f} " @@ -692,7 +731,7 @@ def stage_L(): d = m.P * m.L gp = torch.Generator().manual_seed(11) perms = torch.randperm(d, generator=gp)[None].repeat(m.users, 1) - 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)] oma = oma_ser_jammed(10.0, jsr, bits=int(math.log2(m.V)), U=m.users) rows = [] for i, j in enumerate(jsr): @@ -705,6 +744,31 @@ def stage_L(): write_csv(DATA / "sec_jam_cmp.csv", ["jsr_db", "blind", "matched", "perm_blind", "oma_targeted"], rows) + stage_L_gap(rows) + + +def stage_L_gap(rows): + """Store the blind-vs-matched power gap as a raw artifact. + + For every error level both curves reach, the gap is the extra JSR the + blind jammer needs to inflict it. Both curves are interpolated on the + dense grid, so the quoted range comes from a stored file rather than + from a hand interpolation. + """ + import numpy as np + j = np.array([r[0] for r in rows]) + blind = np.array([r[1] for r in rows]) + matched = np.array([r[2] for r in rows]) + lo = max(blind.min(), matched.min()) + hi = min(blind.max(), matched.max()) + ser = np.linspace(lo, hi, 200) + jb = np.interp(ser, blind, j) + jm = np.interp(ser, matched, j) + gap = jb - jm + write_csv(DATA / "sec_jam_gap.csv", ["ser", "gap_db"], + list(zip(ser.tolist(), gap.tolist()))) + print(f" gap: {gap.min():.2f} to {gap.max():.2f} dB " + f"over SER {lo:.3f} to {hi:.3f}") def main(): diff --git a/code/exp_permkpa.py b/code/exp_permkpa.py new file mode 100644 index 0000000..0c0d13a --- /dev/null +++ b/code/exp_permkpa.py @@ -0,0 +1,99 @@ +"""Known-plaintext attack on the global-permutation key (run under WSL). + +The permutation scheme keeps the masks public and protects the frame +with one secret permutation of the d entries shared by all users. Like +the keyed masking, the protection is linear, so an attacker that knows +the indices a few frames carried can recover the secret. This stage +measures how many known frames the recovery needs, mirroring the grid +of exp_kpa.py so the two curves share one figure. + +Attack: with N known frames the attacker knows the pre-permutation +signal x_n and observes y_n = h_n * perm(x_n) + noise at the collection +SNR. The cross-correlation matrix C[i, j] = sum_n y_n[i] x_n[j] peaks at +j = perm(i) because h_n > 0, so the permutation is the assignment that +maximizes the total correlation, solved by the Hungarian method. The +recovered permutation then decodes user 1 at 10 dB, the convention of +exp_kpa.py. + +Writes data/pkpa.csv. Fixed seeds: permutation 11 (the stage-I secret), +collection 909. +""" +from __future__ import annotations +import math +import numpy as np +import torch + +from sse_lib import write_csv, set_seed, DATA, DEVICE +from exp_full import get_model, eval_scheme_permuted_eve, rayleigh_gain + +try: + from scipy.optimize import linear_sum_assignment +except ImportError: # greedy fallback + def linear_sum_assignment(cost): + c = cost.copy() + n = c.shape[0] + rows = np.empty(n, dtype=int) + cols = np.empty(n, dtype=int) + for k in range(n): + i, j = np.unravel_index(np.argmin(c), c.shape) + rows[k], cols[k] = i, j + c[i, :] = np.inf + c[:, j] = np.inf + order = np.argsort(rows) + return rows[order], cols[order] + +COLLECT_DB = 20.0 +DECODE_DB = 10.0 +TRIALS = 20 +EVAL_FRAMES = 100_000 + + +def main(): + m = get_model(iters=4000) # training needs grad + m.eval() + _run(m) + + +@torch.no_grad() +def _run(m): + d = m.P * m.L + Bn = m.unit_codebook() + true_m = m.masks() + c = m.c + gp = torch.Generator().manual_seed(11) + gperm = torch.randperm(d, generator=gp) + perms = gperm[None].repeat(m.users, 1) + sigma = math.sqrt(1.0 / (d * 10.0 ** (COLLECT_DB / 10.0))) + + print(f"[P] permutation known-plaintext, collect {COLLECT_DB:.0f} dB, " + f"decode {DECODE_DB:.0f} dB ...") + rows = [] + for nf in [1, 2, 3, 4, 5, 6, 8, 10, 12, 16, 24, 32, 48, 64]: + fr, sr = [], [] + for t in range(TRIALS): + g = torch.Generator().manual_seed(909 + 1000 * t + nf) + digits = torch.randint(m.vu, (nf, m.users, m.P), generator=g) + e = Bn[digits.to(DEVICE)] / math.sqrt(m.P) + x = (e * true_m[None, :, None, :]).sum(dim=1) / c # (nf,P,L) + xf = x.reshape(nf, d) + h = rayleigh_gain((nf,), device=DEVICE) + noise = sigma * torch.randn(nf, d, device=DEVICE) + yf = h[:, None] * xf[:, gperm.to(DEVICE)] + noise + C = (yf.T @ xf).cpu().numpy() # (d,d) + _, est = linear_sum_assignment(-C) + est_t = torch.tensor(est, dtype=torch.long) + fr.append(float((est_t == gperm).float().mean())) + sr.append(eval_scheme_permuted_eve( + m, DECODE_DB, EVAL_FRAMES, perms, + eve_perms=est_t[None].repeat(m.users, 1), + seed=777 + 31 * t)) + frac = sum(fr) / len(fr) + ser = sum(sr) / len(sr) + rows.append((nf, frac, ser)) + print(f" N={nf:3d} frac={frac:.4f} eve={ser:.4f}") + write_csv(DATA / "pkpa.csv", ["n_frames", "perm_frac", "eve_ser"], rows) + print("[done] pkpa.csv") + + +if __name__ == "__main__": + main() diff --git a/code/feasibility_security.py b/code/feasibility_security.py index 2ced3eb..174b684 100644 --- a/code/feasibility_security.py +++ b/code/feasibility_security.py @@ -166,7 +166,7 @@ def main(): print("[Q1] Eve no-mask SER:", [f"{v:.3g}" for v in eve_n]) print(f"[Q1] chance frame SER = {chance_frame:.4f}") - write_csv(DATA / "feas_q1_eavesdrop.csv", + write_csv(DATA / "pilot" / "feas_q1_eavesdrop.csv", ["snr_db", "legit", "eve_wrong", "eve_none", "eve_avg", "chance"], [(s, legit[i], eve_w[i], eve_n[i], eve_a[i], chance_frame) for i, s in enumerate(snr_eval)]) @@ -187,7 +187,7 @@ def main(): xc = mean_abs_cross_corr(mdl.masks().detach().cpu()) q2_rows.append((mdl.L, d, lg, ev, xc)) print(f" L={mdl.L:4d} legit={lg:.3g} eve={ev:.3g} |xcorr|={xc:.3f}") - write_csv(DATA / "feas_q2_keyentropy.csv", + write_csv(DATA / "pilot" / "feas_q2_keyentropy.csv", ["L", "d", "legit_ser", "eve_ser", "mask_xcorr"], q2_rows) # Q3: jamming robustness at SNR=10 dB @@ -197,7 +197,7 @@ def main(): jam_rd = eval_ser_jam(model, 10.0, jsr, frames=300_000, mode="random") print("[Q3] aligned-jammer SER:", [f"{v:.3g}" for v in jam_al]) print("[Q3] random-jammer SER:", [f"{v:.3g}" for v in jam_rd]) - write_csv(DATA / "feas_q3_jamming.csv", + write_csv(DATA / "pilot" / "feas_q3_jamming.csv", ["jsr_db", "ser_aligned", "ser_random"], [(j, jam_al[i], jam_rd[i]) for i, j in enumerate(jsr)]) diff --git a/code/replot_security.py b/code/replot_security.py index b7c589b..fed2e8b 100644 --- a/code/replot_security.py +++ b/code/replot_security.py @@ -3,20 +3,22 @@ from ../data/*.csv and writes paper-ready PDFs to ../fig/. No experiment is rerun. All result plots share one canvas and axes rectangle (8:6 box). Label dictionary is fixed here and copied verbatim into tables and prose. - fig_sec_snr.pdf : legitimate and outsider SER vs SNR (Fig. 2) + fig_sec_snr.pdf : legitimate and eavesdropper SER vs SNR (Fig. 2) fig_sec_keylen.pdf : SER vs key length L (Fig. 3) fig_sec_jam.pdf : target-user SER vs JSR, four schemes (Fig. 4) - fig_sec_sens.pdf : outsider SER vs fraction of key held (Fig. 5) - fig_sec_brute.pdf : outsider SER vs number of key guesses (Fig. 6) - fig_sec_kpa.pdf : outsider SER vs known-plaintext frames (Fig. 7) + fig_sec_sens.pdf : eavesdropper SER vs fraction of key held (Fig. 5) + fig_sec_brute.pdf : eavesdropper SER vs number of key guesses (Fig. 6) + fig_sec_kpa.pdf : eavesdropper SER vs known-plaintext frames (Fig. 7) fig_sec_real.pdf : token error rate on real streams (Fig. 8) -fig_sec_brute_rho.pdf is also emitted as a diagnostic and is not used in -the paper. +Curves that coincide by construction are drawn deliberately layered, the +lower one wide and semi-transparent and the upper one narrow with open +markers, so every legend entry has a visible curve. """ from __future__ import annotations from pathlib import Path import csv +import math import matplotlib matplotlib.use("Agg") @@ -32,7 +34,7 @@ plt.rcParams.update({ "font.serif": ["DejaVu Serif", "Times New Roman"], "font.size": 9, "axes.labelsize": 9, - "legend.fontsize": 6.6, + "legend.fontsize": 7.4, "xtick.labelsize": 8, "ytick.labelsize": 8, "axes.grid": True, @@ -57,13 +59,20 @@ C_PUB = "#16a085" LBL = { "legit": "Legitimate", "oma": "OMA", - "eve_pub": "Eve, public masks", - "eve_key": "Eve, wrong key", + "eve_pub": "Eavesdropper, public masks", + "eve_key": "Eavesdropper, wrong key", "chance": "Random guess", - "jam_m": "Matched jammer (public masks)", - "jam_b": "Blind jammer (proposed)", "nojam": "No jammer", + "mask": "Keyed masking", + "perm": "Permutation key", + "pad": "Index cipher", + "insider": "Insider", + "outsider": "Outsider", } +# deliberate-layering style for the LOWER of two coinciding curves +UNDER = dict(lw=2.6, ms=7, alpha=0.85) +# and for the curve riding on top of it +OVER = dict(lw=1.2, ms=4.5, mfc="none") def load(name): @@ -106,10 +115,11 @@ def fig_snr(): r = load("sec_snr.csv") x = col(r, "snr_db") fig, ax = plt.subplots() + # legitimate and OMA coincide by construction; layered deliberately ax.semilogy(x, col(r, "legit"), color=C_LEGIT, marker="o", ls="-", - label=LBL["legit"]) + label=LBL["legit"], **UNDER) ax.semilogy(x, col(r, "oma"), color=C_OMA, marker="^", ls=":", - label=LBL["oma"]) + label=LBL["oma"], **OVER) ax.semilogy(x, col(r, "eve_public"), color=C_PUB, marker="v", ls="none", markersize=5.2, markerfacecolor="none", label=LBL["eve_pub"]) @@ -125,13 +135,17 @@ def fig_snr(): def fig_keylen(): + """The OMA reference is the resource-matched one of oma_ser_keylen, + which is undefined at key lengths where 16/L is not an integer; those + rows carry nan and are skipped.""" r = load("sec_keylen.csv") x = col(r, "L", int) fig, ax = plt.subplots() ax.semilogy(x, col(r, "legit_ser"), color=C_LEGIT, marker="o", ls="-", label=LBL["legit"]) - ax.semilogy(x, col(r, "oma"), color=C_OMA, marker="^", ls=":", - label=LBL["oma"]) + 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], color=C_OMA, + marker="^", ls=":", label=LBL["oma"]) ax.semilogy(x, col(r, "eve_ser"), color=C_EVE, marker="s", ls="--", label=LBL["eve_key"]) ax.set_xlabel("Key length $L$") @@ -144,47 +158,47 @@ def fig_keylen(): def fig_jam(): """Target-user SER against JSR for four schemes. A linear axis is used because the range spans less than one decade, where a log axis - would print wide minor tick labels that crowd out the y label.""" + would print wide minor tick labels that crowd out the y label. The + no-jammer reference is annotated on the line rather than listed in + the legend, so the legend never covers it.""" r = load("sec_jam_cmp.csv") x = col(r, "jsr_db") + me = max(1, len(x) // 8) fig, ax = plt.subplots() - ax.plot(x, col(r, "oma_targeted"), color=C_PUB, marker="^", ls=":", - label="OMA, targeted") ax.plot(x, col(r, "matched"), color=C_MATCH, marker="P", ls="--", - label="Public masks, matched") - # the two blind curves agree to 0.0015, so the proposed one is drawn - # first and wide and the permutation key rides on top with open - # markers, otherwise one legend entry would have no visible curve - ax.plot(x, col(r, "blind"), color=C_LEGIT, marker="o", ls="-", lw=2.6, - ms=7, alpha=0.85, label="Proposed, blind") + markevery=me, label=LBL["mask"] + ", matched") + ax.plot(x, col(r, "oma_targeted"), color=C_PUB, marker="^", ls=":", + markevery=me, label=LBL["oma"] + ", targeted") + # the two blind curves agree to 0.0015; deliberate layering + ax.plot(x, col(r, "blind"), color=C_LEGIT, marker="o", ls="-", + markevery=me, label=LBL["mask"] + ", blind", **UNDER) ax.plot(x, col(r, "perm_blind"), color=C_EVE, marker="s", ls="-.", - lw=1.2, ms=4.5, mfc="none", label="Permutation key, blind") + markevery=me, label=LBL["perm"] + ", blind", **OVER) nojam = float(load("sec_jam.csv")[0]["nojam"]) - ax.axhline(nojam, color=C_OMA, ls=(0, (1, 3)), lw=0.9, - label=LBL["nojam"]) + ax.axhline(nojam, color=C_OMA, ls=(0, (1, 3)), lw=0.9) + ax.text(max(x) - 0.6, nojam + 0.02, LBL["nojam"], ha="right", + va="bottom", fontsize=7.4, color="#555555") ax.set_xlabel("JSR (dB)") ax.set_ylabel("SER") ax.set_xlim(min(x), max(x)) ax.set_ylim(0.2, 1.02) - ax.legend(loc="lower right") + ax.legend(loc="center right", bbox_to_anchor=(0.985, 0.47)) save(fig, "fig_sec_jam") def fig_sens(): """Key sensitivity of three schemes on one axis, the fraction of the - key the attacker holds. For keyed masking that fraction is the mask - correlation, for the permutation scheme the fraction of positions - placed correctly, for the index cipher the fraction of pad bits - known.""" + key the attacker holds. All three ride the random-guess level over + most of the range, so the flat region is deliberately layered.""" r = load("sec_sens_cmp.csv") x = col(r, "frac") fig, ax = plt.subplots() ax.plot(x, col(r, "ser_mask"), color=C_LEGIT, marker="o", ls="-", - label="Keyed masking") + label=LBL["mask"], **UNDER) ax.plot(x, col(r, "ser_perm"), color=C_EVE, marker="s", ls="--", - label="Permutation key") + label=LBL["perm"], **OVER) ax.plot(x, col(r, "ser_pad"), color=C_PUB, marker="v", ls="-.", - label="Index cipher") + lw=1.2, ms=4.5, mfc="none", label=LBL["pad"]) chance = 1.0 - (1.0 / 16.0) ** 4 ax.axhline(chance, color=C_CH, ls=":", lw=0.9, label=LBL["chance"]) ax.set_xlabel("Fraction of the key recovered") @@ -200,54 +214,35 @@ def fig_brute(): r = load("sec_brute_cmp.csv") x = col(r, "K") fig, ax = plt.subplots() - ax.semilogx(x, col(r, "ser_mask"), color=C_LEGIT, marker="o", ls="-", - label="Keyed masking") ax.semilogx(x, col(r, "ser_perm"), color=C_EVE, marker="s", ls="--", - label="Permutation key") + label=LBL["perm"], **UNDER) ax.semilogx(x, col(r, "ser_pad"), color=C_PUB, marker="v", ls="-.", - label="Index cipher") + label=LBL["pad"], **OVER) + ax.semilogx(x, col(r, "ser_mask"), color=C_LEGIT, marker="o", ls="-", + label=LBL["mask"]) kl = load("sec_keylen.csv") legit = float([q for q in kl if int(q["L"]) == 16][0]["legit_ser"]) ax.axhline(legit, color=C_OMA, ls=":", lw=0.9, label=LBL["legit"]) ax.set_xlabel("Number of key guesses $K$") ax.set_ylabel("Eavesdropper SER") - ax.set_ylim(-0.03, 1.05) - ax.legend(loc="center left") + ax.set_ylim(0.2, 1.05) + ax.legend(loc="lower left") save(fig, "fig_sec_brute") -def fig_brute_rho(): - """Best key correlation a search of size K reaches, per key length. - This is a property of the key space alone.""" - r = load("sec_brute.csv") - fig, ax = plt.subplots() - sty = {8: ("#c0392b", "o"), 16: ("#2c5fa8", "s"), - 32: ("#16a085", "v"), 64: ("#8e44ad", "P")} - for Lp, (c, mk) in sty.items(): - rows = [row for row in r if int(row["L"]) == Lp] - ax.semilogx([float(x["K"]) for x in rows], - [float(x["best_rho"]) for x in rows], - color=c, marker=mk, ls="-", label=f"$L={Lp}$") - ax.axhline(0.96, color=C_CH, ls="-.", lw=0.9, label="Break threshold") - ax.set_xlabel("Number of key guesses $K$") - ax.set_ylabel(r"Best key correlation $\kappa$") - ax.set_ylim(0, 1.05) - ax.legend(loc="upper left") - save(fig, "fig_sec_brute_rho") - - def fig_real(): r = load("real_sec_ter.csv") x = col(r, "snr_db") fig, ax = plt.subplots() + # legitimate/OMA and insider/outsider coincide pairwise; layered ax.semilogy(x, col(r, "ter_legit"), color=C_LEGIT, marker="o", ls="-", - label=LBL["legit"]) + label=LBL["legit"], **UNDER) ax.semilogy(x, col(r, "ter_oma"), color=C_OMA, marker="^", ls=":", - label=LBL["oma"]) + label=LBL["oma"], **OVER) ax.semilogy(x, col(r, "ter_insider"), color=C_PUB, marker="v", ls="-.", - label="Insider") + lw=2.6, alpha=0.85, ms=7, label=LBL["insider"]) ax.semilogy(x, col(r, "ter_eve"), color=C_EVE, marker="s", ls="--", - label=LBL["eve_key"]) + label=LBL["outsider"], **OVER) ax.set_xlabel("SNR (dB)") ax.set_ylabel("TER") ax.set_xlim(min(x), max(x)) @@ -256,6 +251,9 @@ def fig_real(): def fig_kpa(): + """Known-plaintext recovery of the keyed masks at three collection + SNRs, with the permutation key under the same attack as the linear + comparison scheme.""" r = load("kpa.csv") fig, ax = plt.subplots() sty = {0.0: ("#c0392b", "o"), 10.0: ("#2c5fa8", "s"), @@ -265,7 +263,13 @@ def fig_kpa(): n = [float(row["n_frames"]) for row in rows] ser = [float(row["eve_ser"]) for row in rows] ax.semilogx(n, ser, color=c, marker=mk, ls="-", - label=f"{int(snr)} dB") + label=LBL["mask"] + f", {int(snr)} dB") + try: + p = load("pkpa.csv") + ax.semilogx(col(p, "n_frames"), col(p, "eve_ser"), color=C_MATCH, + marker="P", ls="--", label=LBL["perm"] + ", 20 dB") + except FileNotFoundError: + print("[skip] pkpa.csv not present yet") # legitimate reference measured with the SAME estimator as the # eavesdropper curves, namely the four-user average of eval_ser_sse # at L=16, taken from sec_keylen.csv rather than from the user-1 @@ -287,7 +291,6 @@ def main(): try: fig_sens() fig_brute() - fig_brute_rho() except FileNotFoundError: print("[skip] attack-difficulty CSVs not present yet") try: diff --git a/code/verify_math.py b/code/verify_math.py index 05bb7eb..af6a9e3 100644 --- a/code/verify_math.py +++ b/code/verify_math.py @@ -33,9 +33,13 @@ def masks(U, d, rng): return M / np.linalg.norm(M, axis=1, keepdims=True) * np.sqrt(d) +ROWS = [] # (tag, claim, emp, err, tol, verdict) + + def report(tag, claim, emp, tol, extra=""): err = abs(claim - emp) ok = err <= tol + ROWS.append((tag, claim, emp, err, tol, "PASS" if ok else "FAIL")) print(f"[{'PASS' if ok else 'FAIL'}] {tag}: claim={claim:.5g} " f"emp={emp:.5g} |err|={err:.2g} tol={tol:g} {extra}") return ok @@ -195,6 +199,16 @@ def main(): } print("\nsummary:", {k: ("PASS" if v else "FAIL") for k, v in results.items()}) print("ALL PASS" if all(results.values()) else "SOME FAILED") + # stored artifact so every quoted verification number has a raw file + import csv as _csv + from pathlib import Path as _Path + data = _Path(__file__).resolve().parents[1] / "data" + with open(data / "verify_math.csv", "w", newline="") as f: + w = _csv.writer(f) + w.writerow(["check", "claim", "empirical", "abs_err", "tol", + "verdict"]) + w.writerows(ROWS) + print("[csv]", data / "verify_math.csv") if __name__ == "__main__": diff --git a/data/pilot/README.txt b/data/pilot/README.txt new file mode 100644 index 0000000..e5c3558 --- /dev/null +++ b/data/pilot/README.txt @@ -0,0 +1,3 @@ +Superseded CPU-sized pilot run (V=256) from feasibility_security.py. +Kept for the record; every quoted number in the paper comes from the +full-scale CSVs one level up or from verify_math.csv. diff --git a/data/feas_q1_eavesdrop.csv b/data/pilot/feas_q1_eavesdrop.csv similarity index 100% rename from data/feas_q1_eavesdrop.csv rename to data/pilot/feas_q1_eavesdrop.csv diff --git a/data/feas_q2_keyentropy.csv b/data/pilot/feas_q2_keyentropy.csv similarity index 100% rename from data/feas_q2_keyentropy.csv rename to data/pilot/feas_q2_keyentropy.csv diff --git a/data/feas_q3_jamming.csv b/data/pilot/feas_q3_jamming.csv similarity index 100% rename from data/feas_q3_jamming.csv rename to data/pilot/feas_q3_jamming.csv diff --git a/data/pkpa.csv b/data/pkpa.csv new file mode 100644 index 0000000..8b67480 --- /dev/null +++ b/data/pkpa.csv @@ -0,0 +1,15 @@ +n_frames,perm_frac,eve_ser +1,0.20234375,0.998977 +2,0.7140625,0.7107255 +3,0.91484375,0.4172725 +4,0.94921875,0.324415 +5,0.9515625,0.340107 +6,0.94765625,0.3034795 +8,0.95625,0.303056 +10,0.95546875,0.3027505 +12,0.94921875,0.3028 +16,0.9546875,0.3036285 +24,0.94921875,0.303151 +32,0.95234375,0.302631 +48,0.95,0.3030415 +64,0.9515625,0.3025125 diff --git a/data/sec_brute_cmp.csv b/data/sec_brute_cmp.csv index 644505e..033cf91 100644 --- a/data/sec_brute_cmp.csv +++ b/data/sec_brute_cmp.csv @@ -1,8 +1,15 @@ K,ser_mask,ser_perm,ser_pad,best_kappa,best_frac -1,0.9993678262,0.9999544349,0.9999847412,0.1909643153,0.0160546875 -10,0.9948030015,0.9999095052,0.9998474121,0.4626973216,0.041015625 -100,0.9825717055,0.9998648567,0.9984741211,0.6342127242,0.0658203125 -1000,0.9568330187,0.9998315989,0.9847412109,0.7432459045,0.084296875 -10000,0.909109588,0.9997988333,0.8474121094,0.8165387856,0.1025 -100000,0.8435049061,0.9997690911,0,0.8680494354,0.1190234375 -1000000,0.7503523409,0.9997409661,0,0.905556646,0.1346484375 +1,0.9993527855,0.9999542171,0.9999893771,0.1909643153,0.0160546875 +3,0.9977393447,0.9999284847,0.9999681312,0.3326517476,0.0281640625 +10,0.9952796283,0.9998968587,0.9998937706,0.450762326,0.043046875 +30,0.9895360243,0.9998741146,0.9996813118,0.5560672497,0.05375 +100,0.9834025595,0.9998495442,0.998937706,0.6290900875,0.0653125 +300,0.9728417994,0.9998307015,0.996813118,0.688703621,0.0741796875 +1000,0.9568452325,0.9998081233,0.9893770599,0.7427389508,0.0848046875 +3000,0.9366731394,0.9997877864,0.9681311798,0.7822538913,0.094375 +10000,0.9075372546,0.9997705208,0.8937705994,0.8169040678,0.1025 +30000,0.8818766363,0.9997525081,0.6813117981,0.8434367197,0.1109765625 +65536,0.8592861449,0.9997413851,0.303815,0.8592030095,0.1162109375 +100000,0.8443657504,0.9997355745,0.303815,0.8678447033,0.1189453125 +300000,0.802269081,0.9997181429,0.303815,0.8874619916,0.1271484375 +1000000,0.7622180175,0.9997021224,0.303815,0.9034448904,0.1346875 diff --git a/data/sec_jam_cmp.csv b/data/sec_jam_cmp.csv index 8b91d4c..ba77a3c 100644 --- a/data/sec_jam_cmp.csv +++ b/data/sec_jam_cmp.csv @@ -1,8 +1,17 @@ jsr_db,blind,matched,perm_blind,oma_targeted -10,0.4691233333,0.7203966667,0.4694833333,0.6401244609 --5,0.6347966667,0.87413,0.6333466667,0.8146859285 -0,0.80602,0.95331,0.8066566667,0.9237966241 -5,0.9194566667,0.98428,0.9189733333,0.9727474174 -10,0.9703833333,0.99481,0.97034,0.9908905586 -15,0.9903833333,0.9983633333,0.9900733333,0.9970319887 -20,0.9967733333,0.9995166667,0.9968766667,0.9990359654 +-8,0.52954,0.7906333333,0.5275833333,0.7152454705 +-6,0.5965433333,0.8503566667,0.59758,0.7840240868 +-4,0.6703433333,0.8956333333,0.6699733333,0.8424432091 +-2,0.7420833333,0.9283666667,0.7431533333,0.8888838836 +0,0.8070433333,0.9532066667,0.8059933333,0.9237966241 +2,0.8593066667,0.96942,0.85862,0.9488822017 +4,0.9014266667,0.9805533333,0.9025733333,0.9662811712 +6,0.9327633333,0.9872533333,0.9318466667,0.9780307416 +8,0.9549433333,0.9917766667,0.9550866667,0.9858109157 +10,0.9699833333,0.99508,0.9707366667,0.9908905586 +12,0.98118,0.9966966667,0.9804366667,0.9941743502 +14,0.9872866667,0.9979833333,0.9879066667,0.9962827887 +16,0.9922966667,0.99871,0.9921133333,0.9976304229 +18,0.9948766667,0.9992,0.99484,0.9984893291 +20,0.99685,0.9994833333,0.9969366667,0.9990359654 diff --git a/data/sec_jam_gap.csv b/data/sec_jam_gap.csv new file mode 100644 index 0000000..971cb50 --- /dev/null +++ b/data/sec_jam_gap.csv @@ -0,0 +1,201 @@ +ser,gap_db +0.7203966667,7.395409349 +0.7217858794,7.394580398 +0.7231750921,7.393751447 +0.7245643049,7.392922496 +0.7259535176,7.392093545 +0.7273427303,7.391264594 +0.728731943,7.390435644 +0.7301211558,7.389606693 +0.7315103685,7.388777742 +0.7328995812,7.387948791 +0.734288794,7.38711984 +0.7356780067,7.386290889 +0.7370672194,7.385461939 +0.7384564322,7.384632988 +0.7398456449,7.383804037 +0.7412348576,7.382975086 +0.7426240704,7.383719533 +0.7440132831,7.386932812 +0.7454024958,7.390146092 +0.7467917085,7.393359371 +0.7481809213,7.39657265 +0.749570134,7.39978593 +0.7509593467,7.402999209 +0.7523485595,7.406212489 +0.7537377722,7.409425768 +0.7551269849,7.412639048 +0.7565161977,7.415852327 +0.7579054104,7.419065607 +0.7592946231,7.422278886 +0.7606838358,7.425492166 +0.7620730486,7.428705445 +0.7634622613,7.431918725 +0.764851474,7.435132004 +0.7662406868,7.438345284 +0.7676298995,7.441558563 +0.7690191122,7.444771843 +0.770408325,7.447985122 +0.7717975377,7.451198402 +0.7731867504,7.454411681 +0.7745759631,7.457624961 +0.7759651759,7.46083824 +0.7773543886,7.46405152 +0.7787436013,7.467264799 +0.7801328141,7.470478079 +0.7815220268,7.473691358 +0.7829112395,7.476904638 +0.7843004523,7.480117917 +0.785689665,7.483331197 +0.7870788777,7.486544476 +0.7884680905,7.489757756 +0.7898573032,7.492971035 +0.7912465159,7.493110679 +0.7926357286,7.4893604 +0.7940249414,7.485610121 +0.7954141541,7.481859841 +0.7968033668,7.478109562 +0.7981925796,7.474359282 +0.7995817923,7.470609003 +0.800971005,7.466858724 +0.8023602178,7.463108444 +0.8037494305,7.459358165 +0.8051386432,7.455607885 +0.8065278559,7.451857606 +0.8079170687,7.454642491 +0.8093062814,7.461282924 +0.8106954941,7.467923358 +0.8120847069,7.474563791 +0.8134739196,7.481204225 +0.8148631323,7.487844658 +0.8162523451,7.494485092 +0.8176415578,7.501125525 +0.8190307705,7.507765959 +0.8204199832,7.514406392 +0.821809196,7.521046826 +0.8231984087,7.527687259 +0.8245876214,7.534327693 +0.8259768342,7.540968126 +0.8273660469,7.54760856 +0.8287552596,7.554248993 +0.8301444724,7.560889427 +0.8315336851,7.567529861 +0.8329228978,7.574170294 +0.8343121106,7.580810728 +0.8357013233,7.587451161 +0.837090536,7.594091595 +0.8384797487,7.600732028 +0.8398689615,7.607372462 +0.8412581742,7.614012895 +0.8426473869,7.620653329 +0.8440365997,7.627293762 +0.8454258124,7.633934196 +0.8468150251,7.640574629 +0.8482042379,7.647215063 +0.8495934506,7.653855496 +0.8509826633,7.653807084 +0.852371876,7.645603621 +0.8537610888,7.637400158 +0.8551503015,7.629196695 +0.8565395142,7.620993232 +0.857928727,7.612789769 +0.8593179397,7.604690194 +0.8607071524,7.609289208 +0.8620963652,7.613888221 +0.8634855779,7.618487234 +0.8648747906,7.623086248 +0.8662640034,7.627685261 +0.8676532161,7.632284274 +0.8690424288,7.636883288 +0.8704316415,7.641482301 +0.8718208543,7.646081314 +0.873210067,7.650680328 +0.8745992797,7.655279341 +0.8759884925,7.659878354 +0.8773777052,7.664477367 +0.8787669179,7.669076381 +0.8801561307,7.673675394 +0.8815453434,7.678274407 +0.8829345561,7.682873421 +0.8843237688,7.687472434 +0.8857129816,7.692071447 +0.8871021943,7.696670461 +0.888491407,7.701269474 +0.8898806198,7.705868487 +0.8912698325,7.7104675 +0.8926590452,7.715066514 +0.894048258,7.719665527 +0.8954374707,7.72426454 +0.8968266834,7.708663797 +0.8982158961,7.689747699 +0.8996051089,7.670831601 +0.9009943216,7.651915503 +0.9023835343,7.648634256 +0.9037727471,7.652417362 +0.9051619598,7.656200468 +0.9065511725,7.659983574 +0.9079403853,7.66376668 +0.909329598,7.667549785 +0.9107188107,7.671332891 +0.9121080235,7.675115997 +0.9134972362,7.678899103 +0.9148864489,7.682682209 +0.9162756616,7.686465314 +0.9176648744,7.69024842 +0.9190540871,7.694031526 +0.9204432998,7.697814632 +0.9218325126,7.701597738 +0.9232217253,7.705380843 +0.924610938,7.709163949 +0.9260001508,7.712947055 +0.9273893635,7.716730161 +0.9287785762,7.712515836 +0.9301677889,7.68932668 +0.9315570017,7.666137524 +0.9329462144,7.647766978 +0.9343354271,7.661181255 +0.9357246399,7.674595531 +0.9371138526,7.688009808 +0.9385030653,7.701424084 +0.9398922781,7.714838361 +0.9412814908,7.728252637 +0.9426707035,7.741666914 +0.9440599162,7.75508119 +0.945449129,7.768495467 +0.9468383417,7.781909743 +0.9482275544,7.79532402 +0.9496167672,7.808738296 +0.9510059799,7.822152572 +0.9523951926,7.835566849 +0.9537844054,7.82423082 +0.9551736181,7.787989163 +0.9565628308,7.801358196 +0.9579520436,7.814727229 +0.9593412563,7.828096263 +0.960730469,7.841465296 +0.9621196817,7.85483433 +0.9635088945,7.868203363 +0.9648981072,7.881572397 +0.9662873199,7.89494143 +0.9676765327,7.908310464 +0.9690657454,7.921679497 +0.9704549581,7.898323164 +0.9718441709,7.896911546 +0.9732333836,7.895499928 +0.9746225963,7.894088311 +0.976011809,7.892676693 +0.9774010218,7.891265075 +0.9787902345,7.889853457 +0.9801794472,7.888441839 +0.98156866,7.824207805 +0.9829578727,7.864499773 +0.9843470854,7.904791741 +0.9857362982,7.945083709 +0.9871255109,7.985375677 +0.9885147236,7.932516304 +0.9899039363,7.872849328 +0.9912931491,7.813182351 +0.9926823618,7.750636227 +0.9940715745,7.986447804 +0.9954607873,8.12093709 +0.99685,7.761658031 diff --git a/data/sec_keylen.csv b/data/sec_keylen.csv index 6c2664e..ac68c79 100644 --- a/data/sec_keylen.csv +++ b/data/sec_keylen.csv @@ -1,10 +1,13 @@ L,d,legit_ser,eve_ser,mask_xcorr,oma -4,16,0.9997925,0.999963,0.01188752614,0.2747696909 -6,24,0.9921175,0.9996935,0.09415384382,0.2747696909 -8,32,0.9297855,0.999972,0.007307400461,0.2747696909 -12,48,0.416604,0.999781,0.005153660662,0.2747696909 +4,16,0.9997925,0.999963,0.01188752614,0.961963405 +6,24,0.9921175,0.9996935,0.09415384382,nan +8,32,0.9297855,0.999972,0.007307400461,0.4769767714 +10,40,0.6965535,0.9999585,0.006223429926,nan +12,48,0.416604,0.999781,0.005153660662,nan +14,56,0.3323575,0.999695,0.005685989745,nan 16,64,0.2762895,0.9999285,0.007116591092,0.2747696909 -24,96,0.1829615,0.999975,0.002973971656,0.2747696909 -32,128,0.131901,0.9998895,0.005575809628,0.2747696909 -48,192,0.090206,0.999845,0.005743456539,0.2747696909 -64,256,0.0635265,0.9997915,0.006678360514,0.2747696909 +20,80,0.2076175,0.9996245,0.003162040841,0.2289444229 +24,96,0.1829615,0.999975,0.002973971656,0.1961714033 +32,128,0.131901,0.9998895,0.005575809628,0.1524639978 +48,192,0.090206,0.999845,0.005743456539,0.1054308944 +64,256,0.0635265,0.9997915,0.006678360514,0.08056383667 diff --git a/data/sec_sens_cmp.csv b/data/sec_sens_cmp.csv index 3d53256..e41d746 100644 --- a/data/sec_sens_cmp.csv +++ b/data/sec_sens_cmp.csv @@ -1,11 +1,14 @@ frac,ser_mask,ser_perm,ser_pad -0,0.99998375,0.9999833333,0.9999847412 -0.2,0.9997954167,0.9996233333,0.999859778 -0.4,0.9985329167,0.9949383333,0.9987114181 -0.6,0.99153625,0.9540933333,0.9881584643 -0.75,0.9644704167,0.880335,0.9375 -0.85,0.88742875,0.7279433333,0.8105354292 -0.9,0.7780379167,0.5716733333,0.6701230223 -0.94,0.6256225,0.4316466667,0.4859430867 -0.97,0.43810125,0.3708666667,0.283022376 -1,0.2756983333,0.3021566667,0 +0,0.9999779167,0.9999883333,0.9999893771 +0.2,0.9997833333,0.9995633333,0.9999023796 +0.4,0.9984945833,0.9950233333,0.9991029086 +0.6,0.9915358333,0.9543866667,0.9917561005 +0.75,0.9643629167,0.8802716667,0.9564884375 +0.85,0.8872583333,0.727755,0.8680976078 +0.9,0.7787070833,0.5709983333,0.7703445963 +0.92,0.7202866667,0.502115,0.7133141438 +0.94,0.6145370833,0.4848966667,0.6421212878 +0.955,0.52254125,0.4416266667,0.5773478672 +0.97,0.4333570833,0.3520316667,0.5008509328 +0.985,0.3505958333,0.30335,0.4105086147 +1,0.2758220833,0.303165,0.303815 diff --git a/data/sec_snr.csv b/data/sec_snr.csv index 80c5a7e..675fbcf 100644 --- a/data/sec_snr.csv +++ b/data/sec_snr.csv @@ -1,7 +1,12 @@ snr_db,legit,eve_wrong,eve_none,eve_public,oma,chance -0,0.8944596875,0.9999515625,0.999990625,0.8943865625,0.8933480658,0.9999847412 -4,0.66980875,0.9999575,0.99998625,0.670624375,0.6686275787,0.9999847412 -8,0.39054875,0.9999425,0.9999865625,0.3902384375,0.3892153151,0.9999847412 -12,0.1880821875,0.9999284375,0.9999890625,0.1877565625,0.1870712987,0.9999847412 -16,0.0811665625,0.9999203125,0.9999875,0.081488125,0.08092517452,0.9999847412 -20,0.033519375,0.999921875,0.999988125,0.0334946875,0.03334949917,0.9999847412 +0,0.8944596875,0.9999621875,0.999988125,0.8945528125,0.8933480658,0.9999847412 +2,0.79877875,0.9999559375,0.999986875,0.798940625,0.7973276257,0.9999847412 +4,0.6697890625,0.9999446875,0.99998625,0.6702265625,0.6686275787,0.9999847412 +6,0.5269903125,0.999944375,0.99999125,0.5272609375,0.525415822,0.9999847412 +8,0.39087,0.9999415625,0.999988125,0.39062875,0.3892153151,0.9999847412 +10,0.2760796875,0.9999296875,0.999986875,0.2753090625,0.2747696909,0.9999847412 +12,0.1876809375,0.9999203125,0.9999884375,0.1878209375,0.1870712987,0.9999847412 +14,0.1250471875,0.999920625,0.9999896875,0.12470875,0.1241256148,0.9999847412 +16,0.0812634375,0.9999153125,0.999985625,0.08121,0.08092517452,0.9999847412 +18,0.05243375,0.9999090625,0.9999865625,0.0526228125,0.05214810026,0.9999847412 +20,0.0335228125,0.9999196875,0.999988125,0.033636875,0.03334949917,0.9999847412 diff --git a/data/verify_math.csv b/data/verify_math.csv new file mode 100644 index 0000000..408bc86 --- /dev/null +++ b/data/verify_math.csv @@ -0,0 +1,10 @@ +check,claim,empirical,abs_err,tol,verdict +V1 legit self-alignment,1.0,0.9994724071424732,0.000527592857526793,0.02,PASS +V2a eve mean advantage,0.0,0.0006469982936097643,0.0006469982936097643,0.003,PASS +V2b eve SER @ 0dB,0.99609375,0.995,0.0010937500000000044,0.015,PASS +V2b eve SER @ 10dB,0.99609375,0.9888333333333333,0.007260416666666658,0.015,PASS +V2b eve SER @ 20dB,0.99609375,0.9881666666666666,0.007927083333333362,0.015,PASS +V2b eve SER @ 80dB,0.99609375,0.9896666666666667,0.0064270833333333055,0.015,PASS +V3b random mask E|corr|,0.09973557010035818,0.10187042771408686,0.002134857613728683,0.009973557010035819,PASS +V4a blind jammer projection mean,0.0,0.00026396107284673695,0.00026396107284673695,0.003,PASS +V4b blind jammer projection variance,0.01558576233568703,0.015476787953278994,0.00010897438240803532,0.0007792881167843516,PASS diff --git a/fig/fig_sec_brute.pdf b/fig/fig_sec_brute.pdf index af49438..46d2b3b 100644 Binary files a/fig/fig_sec_brute.pdf and b/fig/fig_sec_brute.pdf differ diff --git a/fig/fig_sec_brute_rho.pdf b/fig/fig_sec_brute_rho.pdf deleted file mode 100644 index 4b5381c..0000000 Binary files a/fig/fig_sec_brute_rho.pdf and /dev/null differ diff --git a/fig/fig_sec_jam.pdf b/fig/fig_sec_jam.pdf index d57835d..ea0bd01 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 82dbb84..fc4db14 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 0bc771d..5e38bcd 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 6640565..7f4e733 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 099e1cc..fe00e6e 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 dafaf6a..a4b803a 100644 Binary files a/fig/fig_sec_snr.pdf and b/fig/fig_sec_snr.pdf differ