14 Commits

Author SHA1 Message Date
68c79467e4 implement cli for benchmarking and evilnkode 2025-09-09 13:21:27 -05:00
aa2e949938 implement keypad ABC 2025-09-08 12:11:29 -05:00
dkelly
6c07d62cbe implement replay analysis (#7)
Co-authored-by: Donovan <donovan.a.kelly@pm.me>
Reviewed-on: https://git.infra.nkode.tech/dkelly/evilnkode/pulls/7
2025-09-05 16:02:29 +00:00
dkelly
6d07c24c71 Merge pull request 'TowerShuffle' (#6) from TowerShuffle into main
Reviewed-on: https://git.infra.nkode.tech/dkelly/evilnkode/pulls/6
2025-08-29 19:19:19 +00:00
c84b4067d6 create observations 2025-08-29 14:17:38 -05:00
76416aae95 implement tower shuffle 2025-08-29 14:14:37 -05:00
2ef80e8878 implement tower shuffle 2025-08-28 15:47:43 -05:00
dkelly
0b39710a78 Merge pull request 'fix visualnkode' (#5) from BugFixFullShuffle into main
Reviewed-on: https://git.infra.nkode.tech/dkelly/evilnkode/pulls/5
2025-08-21 15:49:44 +00:00
d43a772195 fix visualnkode 2025-08-21 10:49:08 -05:00
dkelly
fecbf3cd19 Merge pull request 'evenly spaced boxes' (#4) from evenlySpacedBoxes into main
Reviewed-on: https://git.infra.nkode.tech/dkelly/evilnkode/pulls/4
2025-08-19 16:11:08 +00:00
e19ae07444 evenly spaced boxes 2025-08-19 11:10:38 -05:00
dkelly
03d5ef14ae Merge pull request 'visualNKode' (#3) from visualNKode into main
Reviewed-on: https://git.infra.nkode.tech/dkelly/evilnkode/pulls/3
2025-08-19 16:06:33 +00:00
4fa7a621d7 implement visualnkode.py 2025-08-19 11:05:05 -05:00
d0329090ac small changes 2025-08-19 09:47:39 -05:00
22 changed files with 1648 additions and 1044 deletions

2
.gitignore vendored
View File

@@ -2,3 +2,5 @@
.DS_Store
output
__pycache__
.ipynb_checkpoints

View File

@@ -1,36 +1,90 @@
# Evil nKode
# Evilnkode Project
Simulated nKode Cracker
This README provides instructions for setting up and running the `evilnkode` project using Conda, activating the environment, and executing the provided CLI scripts. It also covers how to access help for command-line options.
## Installation
## Prerequisites
- Python version 3.10 or greater is required
- Install anaconda (or your preferred tool for environment management)
- **Conda**: Ensure you have Conda installed (Miniconda or Anaconda). Download from [conda.io](https://conda.io).
## Setting Up the Environment
To set up the project environment using the provided `environment.yaml` file, follow these steps:
1. **Install the environment**:
- Ensure you are in the project root directory where `environment.yaml` is located.
- Run the following command to create the `evilnkode` environment:
```bash
conda env create -f environment.yaml
```
- This will install all dependencies specified in `environment.yaml`.
## Activating the Environment
To activate the `evilnkode` environment, run:
### Using conda
```bash
conda env create -f environment.yml
conda activate pynkode
conda activate evilnkode
```
## Starting a Jupyter Notebook
Once activated, your terminal prompt should change to include `(evilnkode)`, indicating the environment is active.
## Running CLI Scripts
The project includes two main CLI scripts: `cli.visualnkode` and `cli.benchmark_histogram`. Below are instructions to run each.
### Running `cli.visualnkode`
To execute the `visualnkode` CLI script:
### Option 1: Using classic Jupyter Notebook
```bash
# Ensure your environment is activated
# For conda: conda activate pynkode
# For pyenv: (should be automatic if in the directory)
# Start the Jupyter Notebook server
jupyter notebook
python -m cli.visualnkode
```
### Option 2: Using JupyterLab
- This command runs the `visualnkode` module from the `cli` package.
- To view available options and arguments, use the `-help` flag:
```bash
python -m cli.visualnkode -help
```
### Running `cli.benchmark_histogram`
To execute the `benchmark_histogram` CLI script:
```bash
# Ensure your environment is activated
# Start JupyterLab
jupyter lab
python -m cli.benchmark_histogram
```
## Notebooks
- [evilnkode](notebooks/evilkode.ipynb)
- This command runs the `benchmark_histogram` module, which may generate output such as benchmark results or histograms. For example, it might produce output like:
```
File exists: output/slidingtowershufflekeypad-6-8-4-5-4-4-10000/benchmark/slidingtowershufflekeypad-6-8-4-5-4-4-10000.pkl
Bench SlidingTowerShuffle Break 5
Bench SlidingTowerShuffle Replay 5
```
- To view available options and arguments, use the `-help` flag:
```bash
python -m cli.benchmark_histogram -help
```
## Using the `-help` Flag
Both CLI scripts (`cli.visualnkode` and `cli.benchmark_histogram`) support a `-help` flag to display available command-line options and their descriptions. Run the following to explore options for each script:
```bash
python -m cli.visualnkode -help
python -m cli.benchmark_histogram -help
```
This will provide detailed information about parameters, flags, and usage for each script.
## Project Structure
Key files and directories in the project:
- `environment.yaml`: Conda environment configuration file.
- `cli/`: Contains CLI scripts (`visualnkode` and `benchmark_histogram`).
- `output/`: Directory where script outputs, such as benchmark results, are stored.
- `src/`: Source code for the project.
- `tests/`: Test scripts for the project.
- `requirements.txt`: Additional dependencies (if needed outside Conda).
For further details, explore the project documentation in the `docs/` directory.

0
__init__.py Normal file
View File

0
cli/__init__.py Normal file
View File

115
cli/benchmark_histogram.py Normal file
View File

@@ -0,0 +1,115 @@
import argparse
from src.benchmark import benchmark
import matplotlib.pyplot as plt
from pathlib import Path
from statistics import mean
from src.keypad.keypad import (
RandomSplitShuffleKeypad,
RandomShuffleKeypad,
SlidingSplitShuffleKeypad,
SlidingTowerShuffleKeypad,
)
def bench_histogram(data, title, number_of_keys, properties_per_key, passcode_len, max_tries_before_lockout, complexity,
disparity, run_count, save_path: Path = None):
min_val = min(data)
max_val = max(data)
bins = range(min_val, max_val + 2)
plt.hist(data, bins=bins, edgecolor='black')
plt.title(title)
plt.xlabel('# of Login Observations')
plt.ylabel('Simulations')
text = (f"number_of_keys={number_of_keys}\n"
f"properties_per_key={properties_per_key}\n"
f"passcode_len={passcode_len}\n"
f"max_tries_before_lockout={max_tries_before_lockout}\n"
f"complexity={complexity}\n"
f"disparity={disparity}\n"
f"run_count={run_count}")
plt.text(0.95, 0.95, text, transform=plt.gca().transAxes, fontsize=10,
verticalalignment='top', horizontalalignment='right', bbox=dict(facecolor='white', alpha=0.5))
if save_path:
save_path = save_path / "histogram"
save_path.mkdir(parents=True, exist_ok=True)
filename = (f"{title.replace(' ', '_')}_keys{number_of_keys}_"
f"props{properties_per_key}_pass{passcode_len}_tries{max_tries_before_lockout}_"
f"comp{complexity}_disp{disparity}_runs{run_count}.png")
plt.savefig(save_path / filename, bbox_inches='tight', dpi=300)
plt.close()
def main():
parser = argparse.ArgumentParser(description='Benchmark Keypad Shuffle')
parser.add_argument('--shuffle_type', type=str,
choices=['RandomSplitShuffle', 'RandomShuffle', 'SlidingSplitShuffle', 'SlidingTowerShuffle'],
default='SlidingTowerShuffle', help='Type of keypad shuffle')
parser.add_argument('--number_of_keys', type=int, default=6, help='Number of keys')
parser.add_argument('--properties_per_key', type=int, default=8, help='Properties per key')
parser.add_argument('--passcode_len', type=int, default=4, help='Passcode length')
parser.add_argument('--max_tries_before_lockout', type=int, default=5, help='Max tries before lockout')
parser.add_argument('--complexity', type=int, default=4, help='Complexity')
parser.add_argument('--disparity', type=int, default=4, help='Disparity')
parser.add_argument('--run_count', type=int, default=10000, help='Number of runs')
parser.add_argument('--output_dir', type=str, default='./output',
help='Output directory for histograms')
args = parser.parse_args()
shuffle_classes = {
'RandomSplitShuffle': RandomSplitShuffleKeypad,
'RandomShuffle': RandomShuffleKeypad,
'SlidingSplitShuffle': SlidingSplitShuffleKeypad,
'SlidingTowerShuffle': SlidingTowerShuffleKeypad
}
keypad_class = shuffle_classes[args.shuffle_type]
keypad = keypad_class.new_keypad(args.number_of_keys, args.properties_per_key)
shuffle_type = str(type(keypad)).lower().split('.')[-1].replace("'>", "")
run_name = f"{shuffle_type}-{args.number_of_keys}-{args.properties_per_key}-{args.passcode_len}-{args.max_tries_before_lockout}-{args.complexity}-{args.disparity}-{args.run_count}"
save_path = Path(args.output_dir) / run_name
bench_result = benchmark(
number_of_keys=args.number_of_keys,
properties_per_key=args.properties_per_key,
passcode_len=args.passcode_len,
max_tries_before_lockout=args.max_tries_before_lockout,
run_count=args.run_count,
complexity=args.complexity,
disparity=args.disparity,
keypad=keypad,
file_path=save_path,
)
print(f"Bench {args.shuffle_type} Break {mean(bench_result.iterations_to_break)}")
print(f"Bench {args.shuffle_type} Replay {mean(bench_result.iterations_to_replay)}")
bench_histogram(
bench_result.iterations_to_break,
f"{args.shuffle_type} Break",
args.number_of_keys,
args.properties_per_key,
args.passcode_len,
args.max_tries_before_lockout,
args.complexity,
args.disparity,
args.run_count,
save_path
)
bench_histogram(
bench_result.iterations_to_replay,
f"{args.shuffle_type} Replay",
args.number_of_keys,
args.properties_per_key,
args.passcode_len,
args.max_tries_before_lockout,
args.complexity,
args.disparity,
args.run_count,
save_path,
)
if __name__ == "__main__":
main()

253
cli/visualnkode.py Normal file
View File

@@ -0,0 +1,253 @@
import json
from typing import Iterable
from dataclasses import dataclass, asdict
from src.evilnkode import Observation
from src.utils import observations, passcode_generator
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from src.keypad.keypad import (
BaseKeypad,
SlidingSplitShuffleKeypad,
SlidingTowerShuffleKeypad,
RandomShuffleKeypad,
RandomSplitShuffleKeypad,
)
import argparse
@dataclass
class ObservationSequence:
target_passcode: list[int]
observations: list[Observation]
def new_observation_sequence(
keypad: BaseKeypad,
passcode_len: int,
complexity: int,
disparity: int,
numb_runs: int,
) -> ObservationSequence:
passcode = passcode_generator(keypad.k, keypad.p, passcode_len, complexity, disparity)
obs_gen = observations(
keypad=keypad,
target_passcode=passcode,
number_of_observations=numb_runs,
)
return ObservationSequence(target_passcode=passcode, observations=[obs for obs in obs_gen])
def _next_json_filename(base_dir: Path) -> Path:
"""Find the next available observation_X.json file in base_dir."""
counter = 1
while True:
candidate = base_dir / f"observation_{counter}.json"
if not candidate.exists():
return candidate
counter += 1
def save_observation_sequence_to_json(seq: ObservationSequence,
filename: Path) -> None:
filename.parent.mkdir(parents=True, exist_ok=True)
with filename.open("w", encoding="utf-8") as f:
json.dump(asdict(seq), f, indent=4)
# ---------- Helpers ----------
def _load_font(preferred: str, size: int) -> ImageFont.FreeTypeFont | ImageFont.ImageFont:
"""Try a preferred TTF, fall back to common monospace, then PIL default."""
candidates = [
preferred,
"DejaVuSansMono.ttf", # common on Linux
"Consolas.ttf", # Windows
"Menlo.ttc", "Menlo.ttf", # macOS
"Courier New.ttf",
]
for c in candidates:
try:
return ImageFont.truetype(c, size)
except Exception:
continue
return ImageFont.load_default()
def _text_size(draw: ImageDraw.ImageDraw, text: str, font: ImageFont.ImageFont) -> tuple[int, int]:
"""Get (w, h) using font bbox for accurate layout."""
left, top, right, bottom = draw.textbbox((0, 0), text, font=font)
return int(right - left), int(bottom - top)
def _join_nums(nums: Iterable[int]) -> str:
return " ".join(str(n) for n in nums)
def _next_available_path(path: Path) -> Path:
"""If path exists, append _1, _2, ..."""
if not path.exists():
return path
base, suffix = path.stem, path.suffix or ".png"
i = 1
while True:
candidate = path.with_name(f"{base}_{i}{suffix}")
if not candidate.exists():
return candidate
i += 1
# ---------- Core rendering ----------
def render_observation_to_png(
target_passcode: list[int],
obs: Observation,
out_path: Path,
*,
header_font_name: str = "DejaVuSans.ttf",
body_font_name: str = "DejaVuSans.ttf",
header_size: int = 28,
body_size: int = 24,
margin: int = 32,
row_padding_xy: tuple[int, int] = (16, 12), # (x, y) padding inside row box
row_spacing: int = 14,
header_spacing: int = 10,
section_spacing: int = 18,
bg_color: str = "white",
fg_color: str = "black",
row_fill: str = "#f7f7f7",
row_outline: str = "#222222",
):
"""
Render a single observation:
- Top lines:
Target Passcode: {target}
Selected Keys: {selected keys}
- Then a stack of row boxes representing the keypad rows.
"""
out_path.parent.mkdir(parents=True, exist_ok=True)
out_path = _next_available_path(out_path)
# Fonts
header_font = _load_font(header_font_name, header_size)
body_font = _load_font(body_font_name, body_size)
# Prepare strings
header1 = f"Target Passcode: {_join_nums(target_passcode)}"
header2 = f"Selected Keys: {_join_nums(obs.key_selection)}"
row_texts = [_join_nums(row) for row in obs.keypad]
# Measure to compute canvas size
# Provisional image for measurement
temp_img = Image.new("RGB", (1, 1), bg_color)
d = ImageDraw.Draw(temp_img)
h1_w, h1_h = _text_size(d, header1, header_font)
h2_w, h2_h = _text_size(d, header2, header_font)
row_text_sizes = [_text_size(d, t, body_font) for t in row_texts]
row_box_widths = [tw + 2 * row_padding_xy[0] for (tw, th) in row_text_sizes]
row_box_heights = [th + 2 * row_padding_xy[1] for (tw, th) in row_text_sizes]
content_width = max([h1_w, h2_w] + (row_box_widths or [0]))
total_rows_height = sum(row_box_heights) + row_spacing * max(0, len(row_box_heights) - 1)
width = content_width + 2 * margin
height = (
margin
+ h1_h
+ header_spacing
+ h2_h
+ section_spacing
+ total_rows_height
+ margin
)
# Create final image
img = Image.new("RGB", (max(width, 300), max(height, 200)), bg_color)
draw = ImageDraw.Draw(img)
# Draw headers
x = margin
y = margin
draw.text((x, y), header1, font=header_font, fill=fg_color)
y += h1_h + header_spacing
draw.text((x, y), header2, font=header_font, fill=fg_color)
y += h2_h + section_spacing
# Draw row boxes with evenly spaced numbers
max_box_width = max(row_box_widths) if row_box_widths else 0
for row, box_h in zip(obs.keypad, row_box_heights):
box_left = x
box_top = y
box_right = x + max_box_width
box_bottom = y + box_h
# draw row rectangle
draw.rectangle(
[box_left, box_top, box_right, box_bottom],
fill=row_fill,
outline=row_outline,
width=2
)
# evenly spaced numbers
n = len(row)
if n > 0:
available_width = max_box_width - 2 * row_padding_xy[0]
spacing = available_width / (n + 1)
for idx, num in enumerate(row, start=1):
num_text = str(num)
num_w, num_h = _text_size(draw, num_text, body_font)
num_x = box_left + row_padding_xy[0] + spacing * idx - num_w / 2
num_y = box_top + (box_h - num_h) // 2
draw.text((num_x, num_y), num_text, font=body_font, fill=fg_color)
y = box_bottom + row_spacing
img.save(out_path, format="PNG")
def _next_run_dir(base_dir: Path) -> Path:
"""Find the next available run directory under base_dir (run_001, run_002, ...)."""
counter = 1
while True:
run_dir = base_dir / f"run_{counter:03d}"
if not run_dir.exists():
run_dir.mkdir(parents=True)
return run_dir
counter += 1
def render_sequence_to_pngs(seq: ObservationSequence, out_dir: Path) -> None:
out_dir.mkdir(parents=True, exist_ok=True)
run_dir = _next_run_dir(out_dir)
for i, obs in enumerate(seq.observations, start=1):
filename = run_dir / f"observation_{i:03d}.png"
render_observation_to_png(seq.target_passcode, obs, filename)
if __name__ == "__main__":
shuffle_classes = {
'RandomSplitShuffle': RandomSplitShuffleKeypad,
'RandomShuffle': RandomShuffleKeypad,
'SlidingSplitShuffle': SlidingSplitShuffleKeypad,
'SlidingTowerShuffle': SlidingTowerShuffleKeypad
}
parser = argparse.ArgumentParser(description="Generate and save observation sequences with optional PNG rendering.")
parser.add_argument("--number-of-keys", type=int, default=6, help="Number of keys in the keypad (default: 6)")
parser.add_argument("--properties-per-key", type=int, default=9, help="Properties per key (default: 9)")
parser.add_argument("--passcode-length", type=int, default=4, help="Length of the passcode (default: 4)")
parser.add_argument("--complexity", type=int, default=0, help="Complexity of the passcode (default: 0)")
parser.add_argument("--disparity", type=int, default=0, help="Disparity of the passcode (default: 0)")
parser.add_argument("--num-runs", type=int, default=50, help="Number of observations to generate (default: 50)")
parser.add_argument("--shuffle-type", type=str, default="SlidingTowerShuffle", choices=list(shuffle_classes.keys()),
help="Keypad shuffle type: 'RandomShuffle' or 'SlidingTowerShuffle' (default: SlidingTowerShuffle)")
parser.add_argument("--output-dir", type=str, default="./output",
help="Custom output directory for JSON and PNG files")
args = parser.parse_args()
keypad = shuffle_classes[args.shuffle_type].new_keypad(6, 9)
obs_seq = new_observation_sequence(keypad, 4, 0, 0, numb_runs=50)
shuffle_type = str(type(keypad)).lower().split('.')[-1].replace("'>", "")
output_dir = Path(args.output_dir)
save_observation_sequence_to_json(obs_seq, output_dir / "obs.json")
render_sequence_to_pngs(obs_seq, output_dir / "obs_png")

View File

@@ -1,19 +1,20 @@
# nKode analysis
# nKode analysis
Luke Oeding (Auburn University)
## What is an nKode?
An nKode consists of a passcode $P$ selected utilizing the following setup:
* a $k$ digit keypad, where each key has:
* $p$ properties (position, central number, color, letter, emoji,...)
* $m$ options for each property (\# positions, \# central numbers, \# colors, \# letters, \# emoji, ... )
- a $k$ digit keypad, where each key has:
- $p$ properties (position, central number, color, letter, emoji,...)
- $m$ options for each property ($\#$ positions, $\#$ central numbers, $\#$ colors, $\#$ letters, $\#$ emoji, ... )
- In principle the number of options for each property doesn't have to be the same, but for the sake of our analysis, we make this uniform choice.
- Typically one wants to have all letters displayed exactly once on the keypad for any instance of a keypad, so we take $m = k$.
- In principle the number of options for each property doesn't have to be the same, but for the sake of our analysis, we make this uniform choice.
- Typically one wants to have all letters displayed exactly once on the keypad for any instance of a keypad, so we take $m = k$.
* $\ell$ = length of the passcode, which is a sequence of $\ell$ letters (options) selected from any of the options.
* a shuffling rule (the split-shuffle)
- $\ell$ = length of the passcode, which is a sequence of $\ell$ letters (options) selected from any of the options.
- a shuffling rule (the split-shuffle)
## Split shuffling
@@ -23,7 +24,7 @@ For each attempt at entering a passcode (except for the initial login challenge)
We would like a function $E = E(k,p,m,\ell)$ of the entropy of the passcode / nKode.
We would like a function $N = N(k,p,m,\ell,s)$ of views of correctly entered nKodes that an intruder would need to learn a passcode from observations.
We would like a function $N = N(k,p,m,\ell,s)$ of views of correctly entered nKodes that an intruder would need to learn a passcode from observations.
## Entropy
@@ -46,14 +47,14 @@ $$ E(m,p,\ell) = \log_2[(mp)^\ell] = \ell \frac{\log(mp)}{\log(2)}$$
So, for example, when $m=10$ and $p = 7$, the alphabet has 70 letters, and a passcode of length $\ell = 4$ would have entropy:
$$ E(10,7,4) = 4\log(70)/\log(2) = 24.5\; \text{bits}$$
$$E(10,7,4) = 4\log(70)/\log(2) = 24.5\; \text{bits}$$
$$ E(10,7,6) = 6\log(70)/\log(2) = 36.8\; \text{bits}$$
$$E(10,7,6) = 6\log(70)/\log(2) = 36.8\; \text{bits}$$
Relevant is the entropy per symbol, which is $ E(m,p,1) = \log_2(m) +\log_2(p) = \frac{\log(m) + \log(p)}{\log(2)}$.
Relevant is the entropy per symbol, which is $E(m,p,1) = \log_2(m) +\log_2(p) = \frac{\log(m) + \log(p)}{\log(2)}$.
For instance:
$ E(10,7,1) = \log(70)/\log(2) = 6.13\; \text{bits}$
$E(10,7,1) = \log(70)/\log(2) = 6.13\; \text{bits}$
### nKode Entropy
@@ -62,16 +63,16 @@ If one is interested in the likelihood of a single attempt randomly entered nKod
The total number of available passcodes with
$\ell$ letters selected from an alphabet with $k$ keys, with replacement is
$$ k^\ell $$
$$k^\ell$$
So the entropy of the one time keypad is
$$ E(k,\ell) = \log_2[(k)^\ell] = \ell \frac{\log(k)}{\log(2)}$$
$$E(k,\ell) = \log_2[(k)^\ell] = \ell \frac{\log(k)}{\log(2)}$$
So, for example, when $k=10$ a passcode of length $\ell = 4$ would have entropy:
$ E(10,4) = 4\log(10)/\log(2) = 13.28\; \text{bits}$
$E(10,4) = 4\log(10)/\log(2) = 13.28\; \text{bits}$
The entropy per symbol, which is $ E(k,1) = \log_2(k) = \frac{\log(k)}{\log(2)}$.
The entropy per symbol, which is $E(k,1) = \log_2(k) = \frac{\log(k)}{\log(2)}$.
In the case $k=10$ we have $E(10,1) = 3.22 \; \text{bits}$.
@@ -81,8 +82,7 @@ We are interested in understanding the number of times an eavesdropping intruder
### Eavesdropper with split shuffles
[Brooks Brown says that the lower bound is $\min\{3, s \log_2+1\}$, where $s$ is the number of attribute sets, for an nKode of complexity $c=1$, regardless of nKode length $l$, or the number of tiles $t$.]
[Brooks Brown says that the lower bound is $\min\{3, s \log_2+1\}$, where $s$ is the number of attribute sets, for an nKode of complexity $c=1$, regardless of nKode length $l$, or the number of tiles $t$.]
Regarding the eavesdropper attack we should also consider the case of a keystroke recorder that doesn't observe the labels on the keys of the nKode keypad.
Blind single attacks and repeated blind attempts that successfully log in, and learning the nKode.
@@ -95,16 +95,11 @@ Blind single attacks and repeated blind attempts that successfully log in, and l
The probability that a (blind) randomly entered key-string will yield a successful login:
$
R = \frac{\text{Num}(\text{passcodes that would yield a correct login})}{\text{Num}(\text{possible passcodes})}.
$
$R = \frac{\#(\text{passcodes that would yield a correct login})}{\#(\text{possible passcodes})}.$
(Num stands for 'number of'.)
This should be simply computed using the number of keys $k$ and the length $l$ of the passcode:
$
R = (1/k)^l.
$
$R = (1/k)^l.$
For example, a 6 digit pin would yield a one-in-a-million chance of blindly hitting the correct passcode.
@@ -114,8 +109,8 @@ For the moment we only consider the case of $p=2$, which could be the case if th
Note that if a user insists on only using the placement value of the keys in an nKode to select their password, then they effectively have reduced the complexity or entropy of their password to that of the normal keypad, and an adversary could use frequency analysis to increase the likelihood of guessing a correct password. However, if the user were to use only the number values on the keypad, they would still only have the entropy of a standard keypad generated password, however, because of the shuffling of the letters, using an nKode provides the user with some protection against frequency analysis attacks in the case of a blind intruder.
For example, it is know that users pick passwords like 1234 or 123456 much more frequently than of any other password. If the intruder is able to observe the user typing one of these passcodes on an nKode keypad, then they would be able to guess the passcode with higher probability than random. However, if the intruder is only able to record keystrokes, but not see the display of the nKode keypad, then the intruder would only guess the correct passcode at the frequency of guessing a permutation of the correct passcode. In the case of $\ell $ consecutive digits, the keystroke intruder would only observe $\ell$ distinct keys being typed. The number of such passcodes is $\binom{k}{\ell}$. So in the case of a $k=10$ digit keypad, the number of $\ell=4$ digit passcodes with distinct entries is $\binom{10}{4} = 210$, and when $\ell = 6$ we also have $\binom{10}{6} = 210$. So, after one observation indicating that the passcode consists of $\ell$ distinct digits, the intruder would have probability $P = 1/\binom{k}{\ell}$ of guessing the passcode.
It is known that the expected number of trials until the first success would be $1/P$. In this case it would take the intruder on average $\binom{k}{\ell}$ attempts to successfully log in (without guessing the passcode). Even in the case of the 4 digit PIN where the intruder guesses that the passcode consists of the first $4$ digits, (or consecutive or even just distinct) integers the nKode obtains an increase in security for the user by a factor of approximately 210 since the key recording intruder would guess the password 1234 on the first attempt on a standard keypad, but would need approximately 210 trials to successfully log in.
For example, it is know that users pick passwords like 1234 or 123456 much more frequently than of any other password. If the intruder is able to observe the user typing one of these passcodes on an nKode keypad, then they would be able to guess the passcode with higher probability than random. However, if the intruder is only able to record keystrokes, but not see the display of the nKode keypad, then the intruder would only guess the correct passcode at the frequency of guessing a permutation of the correct passcode. In the case of $\ell$ consecutive digits, the keystroke intruder would only observe $\ell$ distinct keys being typed. The number of such passcodes is $\binom{k}{\ell}$. So in the case of a $k=10$ digit keypad, the number of $\ell=4$ digit passcodes with distinct entries is $\binom{10}{4} = 210$, and when $\ell = 6$ we also have $\binom{10}{6} = 210$. So, after one observation indicating that the passcode consists of $\ell$ distinct digits, the intruder would have probability $P = 1/\binom{k}{\ell}$ of guessing the passcode.
It is known that the expected number of trials until the first success would be $1/P$. In this case it would take the intruder on average $\binom{k}{\ell}$ attempts to successfully log in (without guessing the passcode). Even in the case of the 4 digit PIN where the intruder guesses that the passcode consists of the first $4$ digits, (or consecutive or even just distinct) integers the nKode obtains an increase in security for the user by a factor of approximately 210 since the key recording intruder would guess the password 1234 on the first attempt on a standard keypad, but would need approximately 210 trials to successfully log in.
### Multiple blind attempts for a large userbase (Password spraying)
@@ -143,17 +138,13 @@ For example,
### Guessing the passcode:
The likelihood that a randomly chosen passcode will yield a successful login no matter what shuffle has been applied, i.e. so that the attacker can successfully log in as many times as they want:
$1 / \text{Num}(\text{possible passcodes}).$
$1 / \#(\text{possible passcodes}).$
For example, when $k=m=10, p=7$ for $\ell = 4$ this probability is
$(70)^{-4} \sim 4.16*10^{-8},$
or about 4 chances in 100 million, and for $\ell = 6$ this probability is $(70)^{-6} \sim 8.5*10^{-12}$, or about 8.5 chances in 1 trillion.
For example, when $k=m=10, p=7$ for $\ell = 4$ this probability is $(70)^{-4} \sim 4.16*10^{-8}$, or about 4 chances in 100 million, and for $\ell = 6$ this probability is $(70)^{-6} \sim 8.5*10^{-12}$, or about 8.5 chances in 1 trillion.
## Multiple Blind Attempts
The likelihood a blind attempt at a nKode on $k$ keys of length $\ell$ will successfully log in is $1/k^\ell$.
The likelihood a blind attempt at a nKode on $k$ keys of length $\ell$ will successfully log in is $1/k^\ell$.
For example, when $k=10$ for $\ell = 4$ this probability is $(10)^{-4}$, 1 chance in 10 thousand, and for $\ell = 6$ this probability is 1 chance in a million.
@@ -164,6 +155,7 @@ For example, when $k=10$ for $\ell = 4$ and $s=2$ this probability is $((10)^{-4
For example, when $k=10$ for $\ell = 4$ and $s=3$ this probability is $((10)^{-4})^3 = 10^{-12}$, [same order of magnitude as a passcode of length 6], and for $\ell = 6$ this probability is $((10)^{-6})^3 = 10^{-18}$, or 1 chance in one billion billion.
## Incorrect nKodes that still work
We should also consider the number of nKodes that would yield a successful sequence of $s$ logins. [Add example]
## Longer passcodes might not always be more secure
@@ -193,348 +185,3 @@ If someone is able to observe the user typing the nKode, or record the keystroke
### Eye tracking
Eye tracker on phone: How good would this need to be in order to see what attribute the user is searching for?
# Higher Complexity
## Dispersion
Dispersion is an operation on a keypad that permutes properties in such a way that 2 observations of the nKode are sufficient to learn the passcode. It does this by applying a distinct rotation to each property. The authors note that this is possible when the number of keys is not larger than the number of properties per key, because this ensures that there are enough distinct rotations so that no repetitions occur. There are more general permutations that can also have this property, and it seems that this is already implemented in the Enrollment_Login_Renewal.
## Split shuffle
Split shuffle attempts to avoid the dispersion permutation of the keypad so as to increase the number of times an intruder would have to observe the nKode being entered.
The properties are divided into 2 sets, each set will be shuffled by the same shuffle applied to all properties in that set of properties.
Note: by observing both keypads (before and after a split-shuffle), one can learn both what the split was, and what the two shuffles were.
We're intertested in studying how many observations an intruder must make in order to learn the passcode with the split shuffle in place. There are a few scenarios I can imagine.
* No split (analyzed above).
* The split is determined once, and then the shuffles only happen on one side of the split.
* The split changes every time randomly.
* The split changes every time by a set strategy.
Of course we can consider these questions each time the metaparameters change. Recall,
* a $k$ digit keypad,
* $p$ properties (position, central number, color, letter, emoji,...)
* $m$ options for each property (\# positions, \# central numbers, \# colors, \# letters, \# emoji, ... ). typically $m = k$.
* $\ell$ = length of the passcode, which is a sequence of $\ell$ letters (options) selected from any of the options.
Notice that when $k = 1$ the problem is nearly trivial. It doesn't matter what the properties are, the only thing the user is entering is $\ell$, and that can be observed in 1 try.
When $k = 2$. Here's the case $p = 2$ and a 4 letter passcode.
|key | p0 | p1 |
|:----:|:--:|:--:|
|key 0 | a0 | b0 |
|key 1 | a1 | b1 |
After a shuffle [attribute 1]
|key | p0 | p1 |
|:----:|:--:|:--:|
|key 0 | a0 | b1 |
|key 1 | a1 | b0 |
interaction:
|what | |||||
|--------|--|--|--|--|--|
|Passcode| a0|b1|a1|b0|
|Display 1| 0|1|1|0|
|Display 2| 0|0|1|1|
The possible passcodes after display 1:
0{a0,b0},1{a1,b1},1{a1,b1},0{a0,b0}
The possible passcodes after display 2:
0{a0,b1},0{a0,b1},1{a1,b0},1{a1,b0}
Intersect:
{a0},{b1},{a1},{b0}
Passcode learned in 2.
When $k = 2$. Here's the case $p = 4$ and a 4 letter passcode.
|key | p0 | p1 | p2 | p3 |
|:----:|:--:|:--:|:--:|:--:|
|key 0 | a0 | b0 | c0 | d0 |
|key 1 | a1 | b1 | c1 | d1 |
After a shuffle [attribute 1,2]
|key | p0 | p1 | p2 | p3 |
|:----:|:--:|:--:|:--:|:--:|
|key 0 | a0 | b1 | c1 | d0 |
|key 1 | a1 | b0 | c0 | d1 |
interaction:
|what | |||||
|--------|--|--|--|--|--|
|Passcode| a0|c1|c1|d0|
|Display 1| 0|1|1|0|
|Display 2| 0|0|0|0|
The possible passcodes after display 1:
0{a0,b0,c0,d0},1{a1,b1,c1,d1},1{a1,b1,c1,d1},0{a0,b0,c0,d0}
The possible passcodes after display 2:
0{a0,b1,c1,d0},0{a0,b1,c1,d0},0{a0,b1,c1,d0},0{a0,b1,c1,d0}
Intersect:
{a0,d0},{b1,c1},{b1,c1},{a0,d0}.
Passcode is not learned yet.
After a 2nd shuffle [attribute 1,3]
|key | p0 | p1 | p2 | p3 |
|:----:|:--:|:--:|:--:|:--:|
|key 0 | a0 | b1 | c0 | d1 |
|key 1 | a1 | b0 | c1 | d0 |
|what | |||||
|--------|--|--|--|--|--|
|Passcode| a0|c1|c1|d0|
|Display 1| 0|1|1|0|
|Display 2| 0|0|0|0|
|Display 3| 0|1|1|1|
The possible passcodes after display 3:
0{a0,b1,c0,d1},1{a1,b0,c1,d0},1{a1,b0,c1,d0},1{a1,b0,c1,d0}
Intersect:
{a0},{c1},{c1},{d0}.
Passcode learned in 3.
Here's the case $p = 4$ and an 8 letter passcode.
|key | p0 | p1 | p2 | p3 |
|:----:|:--:|:--:|:--:|:--:|
|key 0 | a0 | b0 | c0 | d0 |
|key 1 | a1 | b1 | c1 | d1 |
Shuffle 1 [attribute 1,2]
|key | p0 | p1 | p2 | p3 |
|:----:|:--:|:--:|:--:|:--:|
|key 0 | a0 | b1 | c1 | d0 |
|key 1 | a1 | b0 | c0 | d1 |
Shuffle 2 [attribute 1,3]
|key | p0 | p1 | p2 | p3 |
|:----:|:--:|:--:|:--:|:--:|
|key 0 | a0 | b1 | c0 | d1 |
|key 1 | a1 | b0 | c1 | d0 |
interaction:
|what | ||||||||||
|--------|--|--|--|--|--|--|--|--|--|--|
|Passcode| a0|c1|c1|d0|b1|b0|a1|d0|
|Display 1| 0| 1| 1| 0| 1| 0| 1| 0|
|Display 2| 0| 0| 0| 0| 0| 1| 1| 0|
|Display 3| 0| 1| 1| 1| 0| 1| 1| 1|
The possible passcodes after display 1:
0{a0,b0,c0,d0},1{a1,b1,c1,d1},1{a1,b1,c1,d1},0{a0,b0,c0,d0},
1{a1,b1,c1,d1},0{a0,b0,c0,d0},1{a1,b1,c1,d1},0{a0,b0,c0,d0}
The possible passcodes after display 2:
0{a0,b1,c1,d0},0{a0,b1,c1,d0},0{a0,b1,c1,d0},0{a0,b1,c1,d0},
0{a0,b1,c1,d0},1{a1,b0,c0,d1},1{a1,b0,c0,d1},0{a0,b1,c1,d0}
Intersect:
{a0,d0},{b1,c1},{b1,c1},{a0,d0},{b1,c1},{b0,c0},{a1,d1},{a0,d0}
Passcode is not learned yet.
The possible passcodes after display 3:
0{a0,b1,c0,d1},1{a1,b0,c1,d0},1{a1,b0,c1,d0},1{a1,b0,c1,d0},
0{a0,b1,c0,d1},1{a1,b0,c1,d0},1{a1,b0,c1,d0},1{a1,b0,c1,d0}
Intersect:
{a0},{c1},{c1},{d0},{b1},{b0},{a1},{d0}
Passcode learned in 3.
Here's the case $p = 8$ and an 4 letter passcode.
|key | p0 | p1 | p2 | p3 | p4 | p5 | p6 | p7 |
|:----:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|key 0 | a0 | b0 | c0 | d0 | e0 | f0 | g0 | h0 |
|key 1 | a1 | b1 | c1 | d1 | e1 | f1 | g1 | h1 |
Shuffle 1 [attribute 0,2,4,6] [a,c,e,g]
|key | p0 | p1 | p2 | p3 | p4 | p5 | p6 | p7 |
|:----:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|key 0 | a1 | b0 | c1 | d0 | e1 | f0 | g1 | h0 |
|key 1 | a0 | b1 | c0 | d1 | e0 | f1 | g0 | h1 |
Shuffle 2 [attribute 2,3,4,5] [c,d,e,f]
|key | p0 | p1 | p2 | p3 | p4 | p5 | p6 | p7 |
|:----:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|key 0 | a0 | b0 | c1 | d1 | e1 | f1 | g0 | h0 |
|key 1 | a1 | b1 | c0 | d0 | e0 | f0 | g1 | h1 |
Shuffle 3 [attribute 0,4,5,6] [a,e,f,g]
|key | p0 | p1 | p2 | p3 | p4 | p5 | p6 | p7 |
|:----:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|key 0 | a1 | b0 | c0 | d0 | e1 | f1 | g1 | h0 |
|key 1 | a0 | b1 | c1 | d1 | e0 | f0 | g0 | h1 |
interaction:
|what | ||||||||||
|--------|--|--|--|--|--|--|--|--|--|--|
|Passcode| a1|c1|b0|h0|f0|e1|a1|d0|
|Display 1| 1| 1| 0| 0| 0| 1| 1| 0|
|Display 2| 0| 0| 0| 0| 0| 0| 0| 0|
|Display 3| 1| 0| 0| 0| 1| 0| 1| 1|
|Display 4| 0| 1| 0| 0| 1| 0| 0| 0|
The possible passcodes from display 1:
`1{a1,b1,c1,d1,e1,f1,g1,h1},`
`1{a1,b1,c1,d1,e1,f1,g1,h1},`
`0{a0,b0,c0,d0,e0,f0,g0,h0},`
`0{a0,b0,c0,d0,e0,f0,g0,h0},`
`0{a0,b0,c0,d0,e0,f0,g0,h0},`
`1{a1,b1,c1,d1,e1,f1,g1,h1},`
`1{a1,b1,c1,d1,e1,f1,g1,h1},`
`0{a0,b0,c0,d0,e0,f0,g0,h0}`
The possible passcodes from display 2:
`0{a1,b0,c1,d0,e1,f0,g1,h0},`
`0{a1,b0,c1,d0,e1,f0,g1,h0},`
`0{a1,b0,c1,d0,e1,f0,g1,h0},`
`0{a1,b0,c1,d0,e1,f0,g1,h0},`
`0{a1,b0,c1,d0,e1,f0,g1,h0},`
`0{a1,b0,c1,d0,e1,f0,g1,h0},`
`0{a1,b0,c1,d0,e1,f0,g1,h0},`
`0{a1,b0,c1,d0,e1,f0,g1,h0}`
Intersect:
`1{a1,c1,e1,g1},`
`1{a1,c1,e1,g1},`
`0{b0,d0,f0,h0},`
`0{b0,d0,f0,h0},`
`0{b0,d0,f0,h0},`
`1{a1,c1,e1,g1},`
`1{a1,c1,e1,g1},`
`0{b0,d0,f0,h0}`
Passcode is not learned yet.
Shuffle 2 [attribute 2,3,4,5] [c,d,e,f]
|key | p0 | p1 | p2 | p3 | p4 | p5 | p6 | p7 |
|:----:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|key 0 | a0 | b0 | c1 | d1 | e1 | f1 | g0 | h0 |
|key 1 | a1 | b1 | c0 | d0 | e0 | f0 | g1 | h1 |
So before observing the input for the 3rd attempt:
Hits for each key from prior information:
[1001][1001]
(1/2 are 1's are 1/2 are 0's for every key.)
Now observe the sequence 10001011
The possible passcodes from display 3:
`1{a1,b1,c0,d0,e0,f0,g1,h1},`
`0{a0,b0,c1,d1,e1,f1,g0,h0},`
`0{a0,b0,c1,d1,e1,f1,g0,h0},`
`0{a0,b0,c1,d1,e1,f1,g0,h0},`
`1{a1,b1,c0,d0,e0,f0,g1,h1},`
`0{a0,b0,c1,d1,e1,f1,g0,h0},`
`1{a1,b1,c0,d0,e0,f0,g1,h1},`
`1{a1,b1,c0,d0,e0,f0,g1,h1}`
Intersect with prior information:
`1{a1,g1},`
`0{c1,e1},`
`0{b0,h0},`
`0{b0,h0},`
`1{d0,f0},`
`0{c1,e1},`
`1{a1,g1},`
`1{d0,f0}`
Passcode not learned yet, but after one more shuffle, we think we would learn the passcode.
Shuffle 3 [attribute 0,4,5,6] [a,e,f,g]
|key | p0 | p1 | p2 | p3 | p4 | p5 | p6 | p7 |
|:----:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|
|key 0 | a1 | b0 | c0 | d0 | e1 | f1 | g1 | h0 |
|key 1 | a0 | b1 | c1 | d1 | e0 | f0 | g0 | h1 |
So before observing the input for the 4th attempt:
Hits for each key from prior information:
[0][10][0][0][0][10][0][01]
(So several of the correct keys are identified, only letters in positions 1,5,7 are unknown)
The possible passcodes from display 4:
`0{a1,b0,c0,d0,e1,f1,g1,h0},`
`1{a0,b1,c1,d1,e0,f0,g0,h1},`
`0{a1,b0,c0,d0,e1,f1,g1,h0},`
`0{a1,b0,c0,d0,e1,f1,g1,h0},`
`1{a0,b1,c1,d1,e0,f0,g0,h1},`
`0{a1,b0,c0,d0,e1,f1,g1,h0},`
`0{a1,b0,c0,d0,e1,f1,g1,h0},`
`0{a1,b0,c0,d0,e1,f1,g1,h0},`
Intersect:
`{a1,g1},`
`{c1},`
`{b0,h0},`
`{b0,h0},`
`{f0},`
`{e1},`
`{a1,g1},`
`{d0}`
Passcode only paritally learned after 3 shuffles
- uncertain about positions 0,2,3,6 = {a1 or g1}, {b0 or h0}
- certain about positions 1,4,5,7 = c1,f0,e1,d0
Shuffles:
[],
[attribute 0,2,4,6] [a c e g ]
[attribute 2,3,4,5] [ cdef ]
[attribute 0,4,5,6] [a efg ]
1,7 [b,h] - not shuffled 4 times, so the key pressed was always the same for the 3rd and 4th character.
3 -- shuffled 1 time, not shuffled 3 times
4 -- shuffled 3 times, not shuffled 1 time
2,5,6 -- shuffled 2 times, not shuffled 2 times
Note that this passcode didn't use the letter g,
This experiment makes us think of two learning tasks.
1) The ability to learn any possible passcode character from observations of inputs
2) The liklihood of learning a given passcode of a fixed length.
3) Could you get the correct entry even with partial information?
4) Probabilistic attack for guessing the correct sequence?
First split shuffle evenly splits things, so you don't gain much. Subsequent shuffles will start to bias toward the correct passcode. The trade-off is that if you don't shuffle a position then the intruder can just use the same input as before and can enter the correct key without knowing the correct icon, but if you do shuffle, then the intruder learns more information.
One key Im understanding better now: Theres a trade-off between (1) information being learned by the intruder and (2) the chances that an intruder could key in a correct key-sequence for each subsequent observation / login attempt.
If an attribute is not shuffled, then that increases the chance for (2), and if an attribute is shuffled that increases the chance for (1).
I think that when the selection of the sets is randomized like youre doing, youre getting the optimal trade-off between these two things. However, theres also probably a strategy that might be even more optimal than just randomly choosing a split each time. If each shuffle chooses for its set of half of the attributes: half from the attributes that were previously shuffled and half from the attributes that werent shuffled in the previous shuffle. You could look back essentially log_2(p) steps (where p is the number of attributes) and similarly subdivide. This seems to be a way to balance the tradeoff for consecutive observations.

544
environment.yaml Normal file
View File

@@ -0,0 +1,544 @@
name: base
channels:
- defaults
dependencies:
- _anaconda_depends=2024.10=py312_openblas_0
- aiobotocore=2.12.3=py312hca03da5_0
- aiohappyeyeballs=2.4.3=py312hca03da5_0
- aiohttp=3.10.5=py312h80987f9_0
- aioitertools=0.7.1=pyhd3eb1b0_0
- aiosignal=1.2.0=pyhd3eb1b0_0
- alabaster=0.7.16=py312hca03da5_0
- altair=5.0.1=py312hca03da5_0
- anaconda-anon-usage=0.5.0=py312hd6b623d_100
- anaconda-catalogs=0.2.0=py312hca03da5_1
- anaconda-cli-base=0.4.1=py312hca03da5_1
- anaconda-client=1.13.0=py312hca03da5_0
- anaconda-cloud-auth=0.7.2=py312hca03da5_0
- anaconda-navigator=2.6.4=py312hca03da5_0
- anaconda-project=0.11.1=py312hca03da5_0
- annotated-types=0.6.0=py312hca03da5_0
- anyio=4.6.2=py312hca03da5_0
- aom=3.6.0=h313beb8_0
- appdirs=1.4.4=pyhd3eb1b0_0
- applaunchservices=0.3.0=py312hca03da5_0
- appnope=0.1.3=py312hca03da5_1001
- appscript=1.2.5=py312h80987f9_0
- archspec=0.2.3=pyhd3eb1b0_0
- argon2-cffi=21.3.0=pyhd3eb1b0_0
- argon2-cffi-bindings=21.2.0=py312h80987f9_0
- arrow=1.3.0=py312hca03da5_0
- arrow-cpp=16.1.0=hbc20fb2_0
- astroid=3.2.4=py312hca03da5_0
- astropy=6.1.3=py312h80987f9_0
- astropy-iers-data=0.2024.9.2.0.33.23=py312hca03da5_0
- asttokens=2.0.5=pyhd3eb1b0_0
- async-lru=2.0.4=py312hca03da5_0
- asyncssh=2.17.0=py312hca03da5_0
- atomicwrites=1.4.0=py_0
- attrs=24.2.0=py312hca03da5_0
- automat=20.2.0=py_0
- autopep8=2.0.4=pyhd3eb1b0_0
- aws-c-auth=0.6.19=h80987f9_0
- aws-c-cal=0.5.20=h80987f9_0
- aws-c-common=0.8.5=h80987f9_0
- aws-c-compression=0.2.16=h80987f9_0
- aws-c-event-stream=0.2.15=h313beb8_0
- aws-c-http=0.6.25=h80987f9_0
- aws-c-io=0.13.10=h80987f9_0
- aws-c-mqtt=0.7.13=h80987f9_0
- aws-c-s3=0.1.51=h80987f9_0
- aws-c-sdkutils=0.1.6=h80987f9_0
- aws-checksums=0.1.13=h80987f9_0
- aws-crt-cpp=0.18.16=h313beb8_0
- aws-sdk-cpp=1.10.55=h313beb8_0
- babel=2.11.0=py312hca03da5_0
- backports=1.1=pyhd3eb1b0_1
- backports.functools_lru_cache=1.6.4=pyhd3eb1b0_0
- backports.tempfile=1.0=pyhd3eb1b0_1
- backports.weakref=1.0.post1=py_1
- bcrypt=3.2.0=py312h80987f9_1
- beautifulsoup4=4.12.3=py312hca03da5_0
- binaryornot=0.4.4=pyhd3eb1b0_1
- black=24.8.0=py312hca03da5_0
- blas=1.0=openblas
- bleach=6.2.0=py312hca03da5_0
- blinker=1.6.2=py312hca03da5_0
- blosc=1.21.3=h313beb8_0
- bokeh=3.6.0=py312hca03da5_0
- boltons=23.0.0=py312hca03da5_0
- boost-cpp=1.82.0=h48ca7d4_2
- botocore=1.34.69=py312hca03da5_0
- bottleneck=1.4.2=py312ha86b861_0
- brotli=1.0.9=h80987f9_8
- brotli-bin=1.0.9=h80987f9_8
- brotli-python=1.0.9=py312h313beb8_8
- brunsli=0.1=hc377ac9_1
- bzip2=1.0.8=h80987f9_6
- c-ares=1.19.1=h80987f9_0
- c-blosc2=2.12.0=h7df6c2f_0
- ca-certificates=2024.11.26=hca03da5_0
- cachetools=5.3.3=py312hca03da5_0
- cctools=949.0.1=hc179dcd_25
- cctools_osx-arm64=949.0.1=h332cad3_25
- certifi=2024.8.30=py312hca03da5_0
- cffi=1.17.1=py312h3eb5a62_0
- cfitsio=3.470=h7f6438f_7
- chardet=4.0.0=py312hca03da5_1003
- charls=2.2.0=hc377ac9_0
- charset-normalizer=3.3.2=pyhd3eb1b0_0
- click=8.1.7=py312hca03da5_0
- cloudpickle=3.0.0=py312hca03da5_0
- colorama=0.4.6=py312hca03da5_0
- colorcet=3.1.0=py312hca03da5_0
- comm=0.2.1=py312hca03da5_0
- conda=24.11.0=py312hca03da5_0
- conda-build=24.11.2=py312hca03da5_0
- conda-content-trust=0.2.0=py312hca03da5_1
- conda-index=0.5.0=py312hca03da5_0
- conda-libmamba-solver=24.9.0=pyhd3eb1b0_0
- conda-pack=0.6.0=pyhd3eb1b0_0
- conda-package-handling=2.4.0=py312hca03da5_0
- conda-package-streaming=0.11.0=py312hca03da5_0
- conda-repo-cli=1.0.114=py312hca03da5_0
- conda-token=0.4.0=pyhd3eb1b0_0
- conda-verify=3.4.2=py_1
- constantly=23.10.4=py312hca03da5_0
- contourpy=1.3.1=py312h48ca7d4_0
- cookiecutter=2.6.0=py312hca03da5_0
- cryptography=43.0.3=py312h8026fc7_1
- cssselect=1.2.0=py312hca03da5_0
- curl=8.9.1=h02f6b3c_0
- cycler=0.11.0=pyhd3eb1b0_0
- cyrus-sasl=2.1.28=h9131b1a_1
- cytoolz=0.12.2=py312h80987f9_0
- dask=2024.8.2=py312hca03da5_0
- dask-core=2024.8.2=py312hca03da5_0
- dask-expr=1.1.13=py312hca03da5_0
- datasets=2.19.1=py312hca03da5_0
- datashader=0.16.3=py312hca03da5_0
- dav1d=1.2.1=h80987f9_0
- debugpy=1.6.7=py312h313beb8_0
- decorator=5.1.1=pyhd3eb1b0_0
- defusedxml=0.7.1=pyhd3eb1b0_0
- deprecated=1.2.13=py312hca03da5_0
- diff-match-patch=20200713=pyhd3eb1b0_0
- dill=0.3.8=py312hca03da5_0
- distributed=2024.8.2=py312hca03da5_0
- distro=1.9.0=py312hca03da5_0
- dmglib=0.9.5=py312hca03da5_0
- docstring-to-markdown=0.11=py312hca03da5_0
- docutils=0.18.1=py312hca03da5_3
- et_xmlfile=1.1.0=py312hca03da5_1
- executing=0.8.3=pyhd3eb1b0_0
- expat=2.6.3=h313beb8_0
- filelock=3.13.1=py312hca03da5_0
- flake8=7.1.1=py312hca03da5_0
- flask=3.0.3=py312hca03da5_0
- fmt=9.1.0=h48ca7d4_1
- fonttools=4.51.0=py312h80987f9_0
- freetype=2.12.1=h1192e45_0
- frozendict=2.4.2=py312hca03da5_0
- frozenlist=1.5.0=py312h80987f9_0
- fsspec=2024.3.1=py312hca03da5_0
- future=1.0.0=py312hca03da5_0
- gensim=4.3.3=py312hd77ebd4_0
- gettext=0.21.0=hbdbcc25_2
- gflags=2.2.2=h313beb8_1
- giflib=5.2.2=h80987f9_0
- gitdb=4.0.7=pyhd3eb1b0_0
- gitpython=3.1.43=py312hca03da5_0
- glib=2.78.4=h313beb8_0
- glib-tools=2.78.4=h313beb8_0
- glog=0.5.0=h313beb8_1
- gmp=6.2.1=hc377ac9_3
- greenlet=3.0.1=py312h313beb8_0
- gst-plugins-base=1.14.1=h313beb8_1
- gstreamer=1.14.1=h80987f9_1
- h11=0.14.0=py312hca03da5_0
- h5py=3.12.1=py312h8456320_0
- hdf5=1.12.1=h05c076b_3
- heapdict=1.0.1=pyhd3eb1b0_0
- holoviews=1.20.0=py312hca03da5_0
- httpcore=1.0.2=py312hca03da5_0
- httpx=0.27.0=py312hca03da5_0
- huggingface_hub=0.24.6=py312hca03da5_0
- hvplot=0.11.1=py312hca03da5_0
- hyperlink=21.0.0=pyhd3eb1b0_0
- icu=73.1=h313beb8_0
- idna=3.7=py312hca03da5_0
- imagecodecs=2023.1.23=py312h75b721f_1
- imageio=2.33.1=py312hca03da5_0
- imagesize=1.4.1=py312hca03da5_0
- imbalanced-learn=0.12.3=py312hca03da5_1
- importlib-metadata=8.5.0=py312hca03da5_0
- importlib_metadata=8.5.0=hd3eb1b0_0
- incremental=22.10.0=pyhd3eb1b0_0
- inflection=0.5.1=py312hca03da5_1
- iniconfig=1.1.1=pyhd3eb1b0_0
- intake=2.0.7=py312hca03da5_0
- intervaltree=3.1.0=pyhd3eb1b0_0
- ipykernel=6.29.5=py312hca03da5_0
- ipython=8.27.0=py312hca03da5_0
- ipython_genutils=0.2.0=pyhd3eb1b0_1
- ipywidgets=7.8.1=py312hca03da5_0
- isort=5.13.2=py312hca03da5_0
- itemadapter=0.3.0=pyhd3eb1b0_0
- itemloaders=1.1.0=py312hca03da5_0
- itsdangerous=2.2.0=py312hca03da5_0
- jaraco.classes=3.2.1=pyhd3eb1b0_0
- jedi=0.19.1=py312hca03da5_0
- jellyfish=1.0.1=py312h1bd1ac0_1
- jinja2=3.1.4=py312hca03da5_1
- jmespath=1.0.1=py312hca03da5_0
- joblib=1.4.2=py312hca03da5_0
- jpeg=9e=h80987f9_3
- jq=1.7.1=h80987f9_0
- json5=0.9.25=py312hca03da5_0
- jsonpatch=1.33=py312hca03da5_1
- jsonpointer=2.1=pyhd3eb1b0_0
- jsonschema=4.23.0=py312hca03da5_0
- jsonschema-specifications=2023.7.1=py312hca03da5_0
- jupyter=1.0.0=py312hca03da5_9
- jupyter-lsp=2.2.0=py312hca03da5_0
- jupyter_client=8.6.0=py312hca03da5_0
- jupyter_console=6.6.3=py312hca03da5_1
- jupyter_core=5.7.2=py312hca03da5_0
- jupyter_events=0.10.0=py312hca03da5_0
- jupyter_server=2.14.1=py312hca03da5_0
- jupyter_server_terminals=0.4.4=py312hca03da5_1
- jupyterlab=4.2.5=py312hca03da5_0
- jupyterlab-variableinspector=3.1.0=py312hca03da5_0
- jupyterlab_pygments=0.1.2=py_0
- jupyterlab_server=2.27.3=py312hca03da5_0
- jupyterlab_widgets=1.0.0=pyhd3eb1b0_1
- jxrlib=1.1=h1a28f6b_2
- keyring=24.3.1=py312hca03da5_0
- kiwisolver=1.4.4=py312h313beb8_0
- krb5=1.20.1=hf3e1bf2_1
- lazy_loader=0.4=py312hca03da5_0
- lcms2=2.12=hba8e193_0
- ld64=530=hb29bf3f_25
- ld64_osx-arm64=530=h001ce53_25
- ldid=2.1.5=h20b2a84_3
- lerc=3.0=hc377ac9_0
- libabseil=20240116.2=cxx17_h313beb8_0
- libaec=1.0.4=hc377ac9_1
- libarchive=3.7.4=h8f13d7a_0
- libavif=0.11.1=h80987f9_0
- libboost=1.82.0=h0bc93f9_2
- libbrotlicommon=1.0.9=h80987f9_8
- libbrotlidec=1.0.9=h80987f9_8
- libbrotlienc=1.0.9=h80987f9_8
- libclang=14.0.6=default_h1b80db6_1
- libclang13=14.0.6=default_h24352ff_1
- libcurl=8.9.1=h3e2b118_0
- libcxx=14.0.6=h848a8c0_0
- libdeflate=1.17=h80987f9_1
- libedit=3.1.20230828=h80987f9_0
- libev=4.33=h1a28f6b_1
- libevent=2.1.12=h02f6b3c_1
- libffi=3.4.4=hca03da5_1
- libgfortran=5.0.0=11_3_0_hca03da5_28
- libgfortran5=11.3.0=h009349e_28
- libglib=2.78.4=h0a96307_0
- libgrpc=1.62.2=h62f6fdd_0
- libiconv=1.16=h80987f9_3
- liblief=0.12.3=h313beb8_0
- libllvm14=14.0.6=h19fdd8a_4
- libmamba=1.5.11=haeffa04_0
- libmambapy=1.5.11=py312h15e39b3_0
- libnghttp2=1.57.0=h62f6fdd_0
- libopenblas=0.3.21=h269037a_0
- libpng=1.6.39=h80987f9_0
- libpq=17.0=h02f6b3c_0
- libprotobuf=4.25.3=h514c7bf_0
- libsodium=1.0.18=h1a28f6b_0
- libsolv=0.7.24=h514c7bf_1
- libspatialindex=1.9.3=hc377ac9_0
- libssh2=1.11.1=h3e2b118_0
- libthrift=0.15.0=h73c2103_2
- libtiff=4.5.1=h313beb8_0
- libwebp-base=1.3.2=h80987f9_1
- libxml2=2.13.5=h0b34f26_0
- libxslt=1.1.41=hf4d3faa_0
- libzopfli=1.0.3=hc377ac9_0
- linkify-it-py=2.0.0=py312hca03da5_0
- llvm-openmp=14.0.6=hc6e5704_0
- llvmlite=0.43.0=py312h313beb8_0
- locket=1.0.0=py312hca03da5_0
- lxml=5.3.0=py312h1d4350b_0
- lz4=4.3.2=py312h80987f9_0
- lz4-c=1.9.4=h313beb8_1
- lzo=2.10=h1a28f6b_2
- markdown=3.4.1=py312hca03da5_0
- markdown-it-py=2.2.0=py312hca03da5_1
- markupsafe=2.1.3=py312h80987f9_0
- matplotlib=3.9.2=py312hca03da5_1
- matplotlib-base=3.9.2=py312h7ef442a_1
- matplotlib-inline=0.1.6=py312hca03da5_0
- mccabe=0.7.0=pyhd3eb1b0_0
- mdit-py-plugins=0.3.0=py312hca03da5_0
- mdurl=0.1.0=py312hca03da5_0
- menuinst=2.2.0=py312hca03da5_0
- mistune=2.0.4=py312hca03da5_0
- more-itertools=10.3.0=py312hca03da5_0
- mpc=1.1.0=h8c48613_1
- mpfr=4.0.2=h695f6f0_1
- mpmath=1.3.0=py312hca03da5_0
- msgpack-python=1.0.3=py312h48ca7d4_0
- multidict=6.1.0=py312h80987f9_0
- multipledispatch=0.6.0=py312hca03da5_0
- multiprocess=0.70.15=py312hca03da5_0
- mypy=1.11.2=py312h80987f9_0
- mypy_extensions=1.0.0=py312hca03da5_0
- mysql=8.4.0=h3a6587f_1
- navigator-updater=0.5.1=py312hca03da5_0
- nbclassic=1.1.0=py312hca03da5_0
- nbclient=0.8.0=py312hca03da5_0
- nbconvert=7.16.4=py312hca03da5_0
- nbformat=5.10.4=py312hca03da5_0
- ncurses=6.4=h313beb8_0
- nest-asyncio=1.6.0=py312hca03da5_0
- networkx=3.3=py312hca03da5_0
- nltk=3.9.1=py312hca03da5_0
- notebook=7.2.2=py312hca03da5_1
- notebook-shim=0.2.3=py312hca03da5_0
- numba=0.60.0=py312hd77ebd4_0
- numexpr=2.10.1=py312h5d9532f_0
- numpy=1.26.4=py312h7f4fdc5_0
- numpy-base=1.26.4=py312he047099_0
- numpydoc=1.7.0=py312hca03da5_0
- oniguruma=6.9.7.1=h1a28f6b_0
- openjpeg=2.5.2=h54b8e55_0
- openldap=2.6.4=he7ef289_0
- openpyxl=3.1.5=py312h80987f9_0
- openssl=3.0.15=h80987f9_0
- orc=2.0.1=h937ddfc_0
- overrides=7.4.0=py312hca03da5_0
- packaging=24.1=py312hca03da5_0
- pandas=2.2.3=py312hcf29cfe_0
- pandocfilters=1.5.0=pyhd3eb1b0_0
- panel=1.5.3=py312hca03da5_0
- param=2.1.1=py312hca03da5_0
- parsel=1.8.1=py312hca03da5_0
- parso=0.8.3=pyhd3eb1b0_0
- partd=1.4.1=py312hca03da5_0
- patch=2.7.6=h1a28f6b_1001
- pathspec=0.10.3=py312hca03da5_0
- patsy=0.5.6=py312hca03da5_0
- pcre2=10.42=hb066dcc_1
- pexpect=4.8.0=pyhd3eb1b0_3
- pickleshare=0.7.5=pyhd3eb1b0_1003
- pillow=11.0.0=py312hfaf4e14_0
- pip=24.2=py312hca03da5_0
- pkce=1.0.3=py312hca03da5_0
- pkginfo=1.11.2=py312hca03da5_0
- platformdirs=3.10.0=py312hca03da5_0
- plotly=5.24.1=py312h989b03a_0
- pluggy=1.5.0=py312hca03da5_0
- ply=3.11=py312hca03da5_1
- prometheus_client=0.21.0=py312hca03da5_0
- prompt-toolkit=3.0.43=py312hca03da5_0
- prompt_toolkit=3.0.43=hd3eb1b0_0
- propcache=0.2.0=py312h80987f9_0
- protego=0.1.16=py_0
- protobuf=4.25.3=py312h8472c4a_0
- psutil=5.9.0=py312h80987f9_0
- ptyprocess=0.7.0=pyhd3eb1b0_2
- pure_eval=0.2.2=pyhd3eb1b0_0
- py-cpuinfo=9.0.0=py312hca03da5_0
- py-lief=0.12.3=py312h313beb8_0
- pyarrow=16.1.0=py312hd77ebd4_0
- pyasn1=0.4.8=pyhd3eb1b0_0
- pyasn1-modules=0.2.8=py_0
- pybind11-abi=5=hd3eb1b0_0
- pycodestyle=2.12.1=py312hca03da5_0
- pycosat=0.6.6=py312h80987f9_1
- pycparser=2.21=pyhd3eb1b0_0
- pyct=0.5.0=py312hca03da5_0
- pycurl=7.45.3=py312h02f6b3c_0
- pydantic=2.8.2=py312hca03da5_0
- pydantic-core=2.20.1=py312hf0e4da2_0
- pydantic-settings=2.6.1=py312hca03da5_0
- pydeck=0.8.0=py312hca03da5_2
- pydispatcher=2.0.5=py312hca03da5_3
- pydocstyle=6.3.0=py312hca03da5_0
- pyerfa=2.0.1.4=py312ha86b861_0
- pyflakes=3.2.0=py312hca03da5_0
- pygithub=2.4.0=py312hca03da5_0
- pygments=2.15.1=py312hca03da5_1
- pyjwt=2.9.0=py312hca03da5_0
- pylint=3.2.7=py312hca03da5_0
- pylint-venv=3.0.3=py312hca03da5_0
- pyls-spyder=0.4.0=pyhd3eb1b0_0
- pynacl=1.5.0=py312h80987f9_0
- pyobjc-core=10.1=py312h80987f9_0
- pyobjc-framework-cocoa=10.1=py312hb094c41_0
- pyobjc-framework-coreservices=10.1=py312hdd8dd1f_0
- pyobjc-framework-fsevents=10.1=py312hca03da5_0
- pyodbc=5.1.0=py312h313beb8_0
- pyopenssl=24.2.1=py312hca03da5_0
- pyparsing=3.2.0=py312hca03da5_0
- pyqt=5.15.10=py312h313beb8_0
- pyqt5-sip=12.13.0=py312h80987f9_0
- pyqtwebengine=5.15.10=py312h313beb8_0
- pysocks=1.7.1=py312hca03da5_0
- pytables=3.10.1=py312h905a39b_0
- pytest=7.4.4=py312hca03da5_0
- python=3.12.7=h99e199e_0
- python-dateutil=2.9.0post0=py312hca03da5_2
- python-dotenv=0.21.0=py312hca03da5_0
- python-fastjsonschema=2.20.0=py312hca03da5_0
- python-json-logger=2.0.7=py312hca03da5_0
- python-libarchive-c=5.1=pyhd3eb1b0_0
- python-lmdb=1.4.1=py312h313beb8_0
- python-lsp-black=2.0.0=py312hca03da5_0
- python-lsp-jsonrpc=1.1.2=pyhd3eb1b0_0
- python-lsp-server=1.12.0=py312h989b03a_0
- python-slugify=5.0.2=pyhd3eb1b0_0
- python-tzdata=2023.3=pyhd3eb1b0_0
- python-xxhash=2.0.2=py312h80987f9_1
- python.app=3=py312h80987f9_1
- pytoolconfig=1.2.6=py312hca03da5_0
- pytz=2024.1=py312hca03da5_0
- pyuca=1.2=py312hca03da5_1
- pyviz_comms=3.0.2=py312hca03da5_0
- pywavelets=1.7.0=py312h80987f9_0
- pyyaml=6.0.2=py312h80987f9_0
- pyzmq=25.1.2=py312h313beb8_0
- qdarkstyle=3.2.3=pyhd3eb1b0_0
- qstylizer=0.2.2=py312hca03da5_0
- qt-main=5.15.2=h0917680_11
- qt-webengine=5.15.9=h2903aaf_7
- qtawesome=1.3.1=py312hca03da5_0
- qtconsole=5.6.0=py312hca03da5_0
- qtpy=2.4.1=py312hca03da5_0
- queuelib=1.6.2=py312hca03da5_0
- re2=2022.04.01=hc377ac9_0
- readchar=4.0.5=py312hca03da5_0
- readline=8.2=h1a28f6b_0
- referencing=0.30.2=py312hca03da5_0
- regex=2024.9.11=py312h80987f9_0
- reproc=14.2.4=h313beb8_2
- reproc-cpp=14.2.4=h313beb8_2
- requests=2.32.3=py312hca03da5_1
- requests-file=1.5.1=pyhd3eb1b0_0
- requests-toolbelt=1.0.0=py312hca03da5_0
- responses=0.13.3=pyhd3eb1b0_0
- rfc3339-validator=0.1.4=py312hca03da5_0
- rfc3986-validator=0.1.1=py312hca03da5_0
- rich=13.9.4=py312hca03da5_0
- rope=1.12.0=py312hca03da5_0
- rpds-py=0.10.6=py312h2aea54e_1
- rtree=1.0.1=py312hca03da5_0
- ruamel.yaml=0.18.6=py312h80987f9_0
- ruamel.yaml.clib=0.2.8=py312h80987f9_0
- ruamel_yaml=0.17.21=py312h80987f9_0
- s3fs=2024.3.1=py312hca03da5_0
- safetensors=0.4.5=py312h7805bc0_1
- scikit-image=0.24.0=py312hd77ebd4_0
- scikit-learn=1.5.1=py312hd77ebd4_0
- scipy=1.13.1=py312ha409365_0
- scrapy=2.12.0=py312hca03da5_0
- seaborn=0.13.2=py312hca03da5_0
- semver=3.0.2=py312hca03da5_0
- send2trash=1.8.2=py312hca03da5_0
- service_identity=18.1.0=pyhd3eb1b0_1
- setuptools=75.1.0=py312hca03da5_0
- shellingham=1.5.0=py312hca03da5_0
- sip=6.7.12=py312h313beb8_0
- six=1.16.0=pyhd3eb1b0_1
- smart_open=5.2.1=py312hca03da5_0
- smmap=4.0.0=pyhd3eb1b0_0
- snappy=1.2.1=h313beb8_0
- sniffio=1.3.0=py312hca03da5_0
- snowballstemmer=2.2.0=pyhd3eb1b0_0
- sortedcontainers=2.4.0=pyhd3eb1b0_0
- soupsieve=2.5=py312hca03da5_0
- sphinx=7.3.7=py312hca03da5_0
- sphinxcontrib-applehelp=1.0.2=pyhd3eb1b0_0
- sphinxcontrib-devhelp=1.0.2=pyhd3eb1b0_0
- sphinxcontrib-htmlhelp=2.0.0=pyhd3eb1b0_0
- sphinxcontrib-jsmath=1.0.1=pyhd3eb1b0_0
- sphinxcontrib-qthelp=1.0.3=pyhd3eb1b0_0
- sphinxcontrib-serializinghtml=1.1.10=py312hca03da5_0
- spyder=6.0.1=py312hca03da5_0
- spyder-kernels=3.0.0=py312h989b03a_0
- sqlalchemy=2.0.34=py312hbe2cdee_0
- sqlite=3.45.3=h80987f9_0
- stack_data=0.2.0=pyhd3eb1b0_0
- statsmodels=0.14.2=py312ha86b861_0
- streamlit=1.40.1=py312hca03da5_0
- superqt=0.6.7=py312h989b03a_0
- sympy=1.13.2=py312hca03da5_0
- tabulate=0.9.0=py312hca03da5_0
- tapi=1100.0.11=h8754e6a_1
- tbb=2021.8.0=h48ca7d4_0
- tblib=1.7.0=pyhd3eb1b0_0
- tenacity=9.0.0=py312hca03da5_0
- terminado=0.17.1=py312hca03da5_0
- text-unidecode=1.3=pyhd3eb1b0_0
- textdistance=4.6.3=py312h989b03a_0
- threadpoolctl=3.5.0=py312h989b03a_0
- three-merge=0.1.1=pyhd3eb1b0_0
- tifffile=2023.4.12=py312hca03da5_0
- tinycss2=1.2.1=py312hca03da5_0
- tk=8.6.14=h6ba3021_0
- tldextract=5.1.2=py312hca03da5_0
- tokenizers=0.20.1=py312he2d9c3e_1
- toml=0.10.2=pyhd3eb1b0_0
- tomli=2.0.1=py312hca03da5_1
- tomlkit=0.13.2=py312hca03da5_0
- toolz=0.12.0=py312hca03da5_0
- tornado=6.4.1=py312h80987f9_0
- tqdm=4.66.5=py312h989b03a_0
- traitlets=5.14.3=py312hca03da5_0
- transformers=4.45.2=py312hca03da5_0
- truststore=0.8.0=py312hca03da5_0
- twisted=23.10.0=py312hca03da5_0
- typer=0.9.0=py312hca03da5_0
- typing-extensions=4.11.0=py312hca03da5_0
- typing_extensions=4.11.0=py312hca03da5_0
- tzdata=2024b=h04d1e81_0
- uc-micro-py=1.0.1=py312hca03da5_0
- ujson=5.10.0=py312h313beb8_0
- unicodedata2=15.1.0=py312h80987f9_0
- unidecode=1.3.8=py312hca03da5_0
- unixodbc=2.3.11=h1a28f6b_0
- urllib3=2.2.3=py312hca03da5_0
- utf8proc=2.6.1=h80987f9_1
- w3lib=2.1.2=py312hca03da5_0
- watchdog=4.0.1=py312h80987f9_0
- wcwidth=0.2.5=pyhd3eb1b0_0
- webencodings=0.5.1=py312hca03da5_2
- websocket-client=1.8.0=py312hca03da5_0
- werkzeug=3.0.6=py312hca03da5_0
- whatthepatch=1.0.2=py312hca03da5_0
- wheel=0.44.0=py312hca03da5_0
- widgetsnbextension=3.6.6=py312hca03da5_0
- wrapt=1.14.1=py312h80987f9_0
- wurlitzer=3.0.2=py312hca03da5_0
- xarray=2023.6.0=py312hca03da5_0
- xlwings=0.32.1=py312hca03da5_0
- xxhash=0.8.0=h1a28f6b_3
- xyzservices=2022.9.0=py312hca03da5_1
- xz=5.4.6=h80987f9_1
- yaml=0.2.5=h1a28f6b_0
- yaml-cpp=0.8.0=h313beb8_1
- yapf=0.40.2=py312hca03da5_0
- yarl=1.18.0=py312h80987f9_0
- zeromq=4.3.5=h313beb8_0
- zfp=1.0.0=h313beb8_0
- zict=3.0.0=py312hca03da5_0
- zipp=3.21.0=py312hca03da5_0
- zlib=1.2.13=h18a0788_1
- zlib-ng=2.0.7=h80987f9_0
- zope=1.0=py312hca03da5_1
- zope.interface=7.1.1=py312h80987f9_0
- zstandard=0.23.0=py312h1a4646a_1
- zstd=1.5.6=hfb09047_0
- pip:
- svgpathtools==1.7.0
- svgwrite==1.4.3
prefix: /opt/homebrew/anaconda3

View File

@@ -1,125 +0,0 @@
name: evilnkode
channels:
- defaults
dependencies:
- anyio=4.6.2=py312hca03da5_0
- appnope=0.1.3=py312hca03da5_1001
- argon2-cffi=21.3.0=pyhd3eb1b0_0
- argon2-cffi-bindings=21.2.0=py312h80987f9_0
- asttokens=2.0.5=pyhd3eb1b0_0
- async-lru=2.0.4=py312hca03da5_0
- attrs=24.2.0=py312hca03da5_0
- babel=2.11.0=py312hca03da5_0
- beautifulsoup4=4.12.3=py312hca03da5_0
- bleach=6.2.0=py312hca03da5_0
- brotli-python=1.0.9=py312h313beb8_8
- bzip2=1.0.8=h80987f9_6
- ca-certificates=2024.12.31=hca03da5_0
- certifi=2024.12.14=py312hca03da5_0
- cffi=1.17.1=py312h3eb5a62_0
- charset-normalizer=3.3.2=pyhd3eb1b0_0
- comm=0.2.1=py312hca03da5_0
- debugpy=1.6.7=py312h313beb8_0
- decorator=5.1.1=pyhd3eb1b0_0
- defusedxml=0.7.1=pyhd3eb1b0_0
- executing=0.8.3=pyhd3eb1b0_0
- expat=2.6.3=h313beb8_0
- h11=0.14.0=py312hca03da5_0
- httpcore=1.0.2=py312hca03da5_0
- httpx=0.27.0=py312hca03da5_0
- idna=3.7=py312hca03da5_0
- ipykernel=6.29.5=py312hca03da5_0
- ipython=8.27.0=py312hca03da5_0
- jedi=0.19.1=py312hca03da5_0
- jinja2=3.1.4=py312hca03da5_1
- json5=0.9.25=py312hca03da5_0
- jsonschema=4.23.0=py312hca03da5_0
- jsonschema-specifications=2023.7.1=py312hca03da5_0
- jupyter-lsp=2.2.0=py312hca03da5_0
- jupyter_client=8.6.0=py312hca03da5_0
- jupyter_core=5.7.2=py312hca03da5_0
- jupyter_events=0.10.0=py312hca03da5_0
- jupyter_server=2.14.1=py312hca03da5_0
- jupyter_server_terminals=0.4.4=py312hca03da5_1
- jupyterlab=4.2.5=py312hca03da5_0
- jupyterlab_pygments=0.1.2=py_0
- jupyterlab_server=2.27.3=py312hca03da5_0
- libcxx=14.0.6=h848a8c0_0
- libffi=3.4.4=hca03da5_1
- libsodium=1.0.18=h1a28f6b_0
- markupsafe=2.1.3=py312h80987f9_0
- matplotlib-inline=0.1.6=py312hca03da5_0
- mistune=2.0.4=py312hca03da5_0
- nbclient=0.8.0=py312hca03da5_0
- nbconvert=7.16.4=py312hca03da5_0
- nbformat=5.10.4=py312hca03da5_0
- ncurses=6.4=h313beb8_0
- nest-asyncio=1.6.0=py312hca03da5_0
- notebook=7.2.2=py312hca03da5_1
- notebook-shim=0.2.3=py312hca03da5_0
- openssl=3.0.15=h80987f9_0
- overrides=7.4.0=py312hca03da5_0
- packaging=24.1=py312hca03da5_0
- pandocfilters=1.5.0=pyhd3eb1b0_0
- parso=0.8.3=pyhd3eb1b0_0
- pexpect=4.8.0=pyhd3eb1b0_3
- pip=24.2=py312hca03da5_0
- platformdirs=3.10.0=py312hca03da5_0
- prometheus_client=0.21.0=py312hca03da5_0
- prompt-toolkit=3.0.43=py312hca03da5_0
- prompt_toolkit=3.0.43=hd3eb1b0_0
- psutil=5.9.0=py312h80987f9_0
- ptyprocess=0.7.0=pyhd3eb1b0_2
- pure_eval=0.2.2=pyhd3eb1b0_0
- pycparser=2.21=pyhd3eb1b0_0
- pygments=2.15.1=py312hca03da5_1
- pysocks=1.7.1=py312hca03da5_0
- python=3.12.7=h99e199e_0
- python-dateutil=2.9.0post0=py312hca03da5_2
- python-fastjsonschema=2.20.0=py312hca03da5_0
- python-json-logger=2.0.7=py312hca03da5_0
- pytz=2024.1=py312hca03da5_0
- pyyaml=6.0.2=py312h80987f9_0
- pyzmq=25.1.2=py312h313beb8_0
- readline=8.2=h1a28f6b_0
- referencing=0.30.2=py312hca03da5_0
- requests=2.32.3=py312hca03da5_1
- rfc3339-validator=0.1.4=py312hca03da5_0
- rfc3986-validator=0.1.1=py312hca03da5_0
- rpds-py=0.10.6=py312h2aea54e_1
- send2trash=1.8.2=py312hca03da5_0
- setuptools=75.1.0=py312hca03da5_0
- six=1.16.0=pyhd3eb1b0_1
- sniffio=1.3.0=py312hca03da5_0
- soupsieve=2.5=py312hca03da5_0
- sqlite=3.45.3=h80987f9_0
- stack_data=0.2.0=pyhd3eb1b0_0
- terminado=0.17.1=py312hca03da5_0
- tinycss2=1.2.1=py312hca03da5_0
- tk=8.6.14=h6ba3021_0
- tornado=6.4.1=py312h80987f9_0
- traitlets=5.14.3=py312hca03da5_0
- typing-extensions=4.11.0=py312hca03da5_0
- typing_extensions=4.11.0=py312hca03da5_0
- tzdata=2024b=h04d1e81_0
- urllib3=2.2.3=py312hca03da5_0
- wcwidth=0.2.5=pyhd3eb1b0_0
- webencodings=0.5.1=py312hca03da5_2
- websocket-client=1.8.0=py312hca03da5_0
- wheel=0.44.0=py312hca03da5_0
- xz=5.4.6=h80987f9_1
- yaml=0.2.5=h1a28f6b_0
- zeromq=4.3.5=h313beb8_0
- zlib=1.2.13=h18a0788_1
- pip:
- contourpy==1.3.1
- cycler==0.12.1
- fonttools==4.55.3
- iniconfig==2.0.0
- kiwisolver==1.4.7
- matplotlib==3.9.3
- numpy==2.1.3
- pillow==11.0.0
- pluggy==1.5.0
- pyparsing==3.2.0
- pytest==8.3.4

File diff suppressed because one or more lines are too long

199
notebooks/evilnkode.ipynb Normal file

File diff suppressed because one or more lines are too long

View File

@@ -1,84 +1,19 @@
from src.evilkode import Observation, Evilkode
from src.keypad import Keypad
import random
from tqdm import tqdm
from src.evilnkode import EvilNKode
from dataclasses import dataclass
from statistics import mean, variance
from enum import Enum
from src.utils import observations, passcode_generator
from src.keypad.keypad import BaseKeypad
from pathlib import Path
import pickle
@dataclass
class Benchmark:
mean: int
variance: int
runs: list[int]
class ShuffleTypes(Enum):
FULL_SHUFFLE = "FULL_SHUFFLE"
SPLIT_SHUFFLE = "SPLIT_SHUFFLE"
def observations(number_of_keys, properties_per_key, passcode_len, complexity: int, disparity: int, shuffle_type: ShuffleTypes):
k = number_of_keys
p = properties_per_key
n = passcode_len
passcode = passcode_generator(k, p, n, complexity, disparity)
keypad = Keypad.new_keypad(k, p)
def obs_gen():
for _ in range(100): # finite number of yields
yield Observation(
keypad=keypad.keypad.copy(),
key_selection=keypad.key_entry(target_passcode=passcode)
)
match shuffle_type:
case ShuffleTypes.FULL_SHUFFLE:
keypad.full_shuffle()
case ShuffleTypes.SPLIT_SHUFFLE:
keypad.split_shuffle()
case _:
raise Exception(f"no shuffle type {shuffle_type}")
return obs_gen()
def passcode_generator(k: int, p: int, n: int, c: int, d: int) -> list[int]:
assert n >= c
assert p*k >= c
assert n >= d
assert p >= d
passcode_prop = []
passcode_set = []
valid_choices = {i for i in range(k*p)}
repeat_set = n-d
repeat_prop = n-c
prop_added = set()
set_added = set()
for _ in range(n):
prop = random.choice(list(valid_choices))
prop_set = prop//p
passcode_prop.append(prop)
passcode_set.append(prop_set)
if prop in prop_added:
repeat_prop -= 1
if prop_set in set_added:
repeat_set -= 1
prop_added.add(prop)
set_added.add(prop_set)
if repeat_prop <= 0:
valid_choices -= prop_added
if repeat_set <= 0:
for el in valid_choices.copy():
if el // p in set_added:
valid_choices.remove(el)
return passcode_prop
iterations_to_break: list[int]
iterations_to_replay: list[int]
def shuffle_benchmark(
def benchmark(
number_of_keys: int,
properties_per_key: int,
passcode_len: int,
@@ -86,84 +21,44 @@ def shuffle_benchmark(
run_count: int,
complexity: int,
disparity: int,
shuffle_type: ShuffleTypes,
file_path: str = '../output',
keypad: BaseKeypad,
file_path: Path = '../output',
overwrite: bool = False
) -> Benchmark:
file_name = f"{shuffle_type.name.lower()}-{number_of_keys}-{properties_per_key}-{passcode_len}-{max_tries_before_lockout}-{complexity}-{disparity}-{run_count}.txt"
full_path = Path(file_path) / file_name
shuffle_type = str(type(keypad)).lower().split('.')[-1].replace("'>", "")
file_name = f"{shuffle_type}-{number_of_keys}-{properties_per_key}-{passcode_len}-{max_tries_before_lockout}-{complexity}-{disparity}-{run_count}.pkl"
full_path = Path(file_path) / "benchmark" / file_name
if not overwrite and full_path.exists():
print(f"file exists {file_path}")
print(f"File exists: {full_path}")
with open(full_path, "rb") as fp:
return pickle.load(fp)
with open(full_path, "r") as fp:
runs = fp.readline()
runs = runs.split(',')
runs = [int(i) for i in runs]
return Benchmark(
mean=mean(runs),
variance=variance(runs),
runs=runs
)
runs = []
for _ in range(run_count):
evilkode = Evilkode(
iterations_to_break = []
iterations_to_replay = []
for _ in tqdm(range(run_count)):
passcode = passcode_generator(number_of_keys, properties_per_key, passcode_len, complexity, disparity)
evilnkode = EvilNKode(
observations=observations(
number_of_keys=number_of_keys,
properties_per_key=properties_per_key,
passcode_len=passcode_len,
complexity=complexity,
disparity=disparity,
shuffle_type=shuffle_type,
target_passcode=passcode,
keypad=keypad,
),
number_of_keys=number_of_keys,
properties_per_key=properties_per_key,
passcode_len=passcode_len,
max_tries_before_lockout=max_tries_before_lockout,
)
evilout = evilkode.run()
runs.append(evilout.iterations)
evilout = evilnkode.run()
iterations_to_break.append(evilout.iterations_to_break)
iterations_to_replay.append(evilout.iterations_to_replay)
full_path.parent.mkdir(parents=True, exist_ok=True)
with open(full_path, "w") as fp:
fp.write(",".join([str(i) for i in runs])),
return Benchmark(
mean=mean(runs),
variance=variance(runs),
runs=runs
benchmark_result = Benchmark(
iterations_to_break=iterations_to_break,
iterations_to_replay=iterations_to_replay
)
if file_path:
full_path.parent.mkdir(parents=True, exist_ok=True)
with open(full_path, "wb") as fp:
pickle.dump(benchmark_result, fp)
def full_shuffle_benchmark(
number_of_keys: int,
properties_per_key: int,
passcode_len: int,
max_tries_before_lockout: int,
run_count: int,
complexity: int,
disparity: int,
) -> Benchmark:
runs = []
for _ in range(run_count):
evilkode = Evilkode(
observations=observations(
number_of_keys=number_of_keys,
properties_per_key=properties_per_key,
passcode_len=passcode_len,
complexity=complexity,
disparity=disparity,
shuffle_type=ShuffleTypes.FULL_SHUFFLE,
),
number_of_keys=number_of_keys,
properties_per_key=properties_per_key,
passcode_len=passcode_len,
max_tries_before_lockout=max_tries_before_lockout,
)
evilout = evilkode.run()
runs.append(evilout.iterations)
return Benchmark(
mean=mean(runs),
variance=variance(runs),
runs=runs
)
return benchmark_result

View File

@@ -12,19 +12,19 @@ class Observation:
def property_list(self) -> list[set[int]]:
return [set(self.keypad[idx]) for idx in self.key_selection]
@property
def flat_keypad(self) -> list[int]:
return [num for row in self.keypad for num in row]
@dataclass
class EvilOutput:
possible_nkodes: list[list[int]]
iterations: int
@property
def number_of_possible_nkode(self):
return math.prod([len(el) for el in self.possible_nkodes])
iterations_to_break: int
iterations_to_replay: int
@dataclass
class Evilkode:
class EvilNKode:
observations: Iterator[Observation]
passcode_len: int
number_of_keys: int
@@ -32,17 +32,31 @@ class Evilkode:
max_tries_before_lockout: int = 5
possible_nkode = None
def initialize(self):
possible_values = set(range(self.number_of_keys * self.properties_per_key))
self.possible_nkode = [possible_values.copy() for _ in range(self.passcode_len)]
def run(self) -> EvilOutput:
self.initialize()
iterations_to_replay = None
for idx, obs in enumerate(self.observations):
if iterations_to_replay is None:
replay_possibilities = self.replay_attack(obs)
if replay_possibilities <= self.max_tries_before_lockout:
iterations_to_replay = idx + 1
if math.prod([len(el) for el in self.possible_nkode]) <= self.max_tries_before_lockout:
return EvilOutput(possible_nkodes=[list(el) for el in self.possible_nkode], iterations=idx+1)
assert iterations_to_replay <= idx + 1
return EvilOutput(
# possible_nkodes=[list(el) for el in self.possible_nkode],
iterations_to_break=idx + 1,
iterations_to_replay=iterations_to_replay
)
for jdx, props in enumerate(obs.property_list):
self.possible_nkode[jdx] = props.intersection(self.possible_nkode[jdx])
raise Exception("error in Evilkode, observations stopped yielding")
def replay_attack(self, obs: Observation) -> int:
possible_combos = 1
for el in self.possible_nkode:
possible_combos *= len({obs.flat_keypad.index(el2) // self.properties_per_key for el2 in el})
return possible_combos

View File

@@ -1,83 +0,0 @@
from dataclasses import dataclass
import numpy as np
@dataclass
class Keypad:
keypad: np.ndarray
k: int # number of keys
p: int # properties per key
keypad_cache: list #
max_cache_size: int = 100
@staticmethod
def new_keypad(k: int, p: int):
total_properties = k * p
array = np.arange(total_properties)
# Reshape into a 3x4 matrix
keypad = array.reshape(k, p)
set_view = keypad.T
for set_idx in set_view:
np.random.shuffle(set_idx)
return Keypad(keypad=set_view.T, k=k, p=p, keypad_cache=[])
def split_shuffle(self):
"""
This is a modified split shuffle.
It doesn't shuffle the keys only the properties in the keys.
Shuffling the keys makes it hard for people to guess an nKode not a machine.
This split shuffle includes a cache to prevent the same configuration from being used.
This cache is not in any other implementation.
Testing suggests it's not necessary.
Getting the same keypad twice over 100 shuffles is very unlikely.
"""
shuffled_sets = self._shuffle()
# Sort the shuffled sets by the first column
sorted_set = shuffled_sets[np.argsort(shuffled_sets[:, 0])]
while str(sorted_set) in self.keypad_cache:
# continue shuffling until we get a unique configuration
shuffled_sets = self._shuffle()
sorted_set = shuffled_sets[np.argsort(shuffled_sets[:, 0])]
self.keypad_cache.append(str(sorted_set))
self.keypad_cache = self.keypad_cache[:self.max_cache_size]
self.keypad = shuffled_sets
def _shuffle(self) -> np.ndarray:
column_permutation = np.random.permutation(self.p)
column_subset = column_permutation[:self.p // 2]
perm_indices = np.random.permutation(self.k)
shuffled_sets = self.keypad.copy()
shuffled_sets[:, column_subset] = shuffled_sets[perm_indices, :][:, column_subset]
return shuffled_sets
def full_shuffle(self):
shuffled_matrix = np.array([np.random.permutation(row) for row in self.keypad.T])
self.keypad = shuffled_matrix.T
def key_entry(self, target_passcode: list[int]) -> list[int]:
"""
Given target_values, return the row indices they are in.
Assert that each element is >= 0 and < self.k * self.p.
"""
# Convert the list to a NumPy array for vectorized checks
vals = np.array(target_passcode)
# Validate that each value is within the valid range
if np.any((vals < 0) | (vals >= self.k * self.p)):
raise ValueError("One or more values are out of the valid range.")
# Flatten the keypad to a 1D array
flat = self.keypad.flatten()
# Create an inverse mapping from value -> row index
inv_index = np.empty(self.k * self.p, dtype=int)
# Each value v is at position i in 'flat', so row = i // p
for i, v in enumerate(flat):
inv_index[v] = i // self.p
# Use the inverse mapping to get row indices for all target values
return inv_index[vals].tolist()

0
src/keypad/__init__.py Normal file
View File

123
src/keypad/keypad.py Normal file
View File

@@ -0,0 +1,123 @@
from dataclasses import dataclass
import numpy as np
from src.keypad.tower_shuffle import TowerShuffle
from abc import ABC, abstractmethod
from typing import Self
class BaseKeypad(ABC):
keypad: np.ndarray
k: int # number of keys
p: int # properties per key
@classmethod
def _build_keypad(cls, k: int, p: int) -> np.ndarray:
rng = np.random.default_rng()
total = k * p
array = np.arange(total)
keypad = array.reshape(k, p)
set_view = keypad.T.copy()
for set_idx in set_view:
rng.shuffle(set_idx)
return set_view.T
@classmethod
@abstractmethod
def new_keypad(cls, k: int, p: int) -> Self:
raise NotImplementedError
def key_entry(self, target_passcode: list[int]) -> list[int]:
vals = np.array(target_passcode)
if np.any((vals < 0) | (vals >= self.k * self.p)):
raise ValueError("One or more values are out of the valid range.")
flat = self.keypad.flatten()
inv_index = np.empty(self.k * self.p, dtype=int)
for i, v in enumerate(flat):
inv_index[v] = i // self.p
return inv_index[vals].tolist()
@abstractmethod
def shuffle(self):
pass
def keypad_mat(self) -> list[list[int]]:
return [el.tolist() for el in self.keypad]
@dataclass
class SlidingTowerShuffleKeypad(BaseKeypad):
keypad: np.ndarray
k: int # number of keys
p: int # properties per key
tower_shuffle: TowerShuffle
@classmethod
def new_keypad(cls, k: int, p: int) -> Self:
kp = cls._build_keypad(k, p)
return cls(keypad=kp, k=k, p=p, tower_shuffle=TowerShuffle.new(p))
def shuffle(self):
selected_positions = self.tower_shuffle.left_tower.tolist()
shift = np.random.randint(1, self.k) # random int in [1, k-1]
new_key_idxs = np.roll(np.arange(self.k), shift)
shuffled_sets = self.keypad.copy()
shuffled_sets[:, selected_positions] = shuffled_sets[new_key_idxs, :][:, selected_positions]
self.keypad = shuffled_sets
self.tower_shuffle.shuffle()
@dataclass
class RandomShuffleKeypad(BaseKeypad):
keypad: np.ndarray
k: int # number of keys
p: int # properties per key
@classmethod
def new_keypad(cls, k: int, p: int) -> Self:
kp = cls._build_keypad(k, p)
return cls(keypad=kp, k=k, p=p)
def shuffle(self):
shuffled_matrix = np.array([np.random.permutation(row) for row in self.keypad.T])
self.keypad = shuffled_matrix.T
@dataclass
class RandomSplitShuffleKeypad(BaseKeypad):
keypad: np.ndarray
k: int # number of keys
p: int # properties per key
@classmethod
def new_keypad(cls, k: int, p: int) -> Self:
kp = cls._build_keypad(k, p)
return cls(keypad=kp, k=k, p=p)
def shuffle(self):
column_permutation = np.random.permutation(self.p)
column_subset = column_permutation[:self.p // 2]
new_key_idxs = np.random.permutation(self.k)
shuffled_sets = self.keypad.copy()
shuffled_sets[:, column_subset] = shuffled_sets[new_key_idxs, :][:, column_subset]
self.keypad = shuffled_sets
@dataclass
class SlidingSplitShuffleKeypad(BaseKeypad):
keypad: np.ndarray
k: int # number of keys
p: int # properties per key
@classmethod
def new_keypad(cls, k: int, p: int) -> Self:
kp = cls._build_keypad(k, p)
return cls(keypad=kp, k=k, p=p)
def shuffle(self):
selected_positions = np.random.permutation(self.p)
column_subset = selected_positions[:self.p // 2]
shift = np.random.randint(1, self.k)
new_key_idxs = np.roll(np.arange(self.k), shift)
shuffled_sets = self.keypad.copy()
shuffled_sets[:, column_subset] = shuffled_sets[new_key_idxs, :][:, column_subset]
self.keypad = shuffled_sets

View File

@@ -0,0 +1,80 @@
from dataclasses import dataclass
import numpy as np
@dataclass
class Tower:
floors: list[np.ndarray]
def split_tower(self) -> tuple[np.ndarray, np.ndarray]:
discard = np.array([], dtype=int)
keep = np.array([], dtype=int)
balance = self.balance()
for idx, floor in enumerate(self.floors):
div = len(floor)//2 + balance[idx]
floor_shuffle = np.random.permutation(len(floor))
keep = np.concatenate((keep, floor[floor_shuffle[:div]]))
discard = np.concatenate((discard, floor[floor_shuffle[div:]]))
diff = len(discard) - len(keep)
assert 0 <= diff <= 1
return keep, discard
def balance(self) -> list[int]:
odd_floors = np.array([idx for idx, el in enumerate(self.floors) if len(el) & 1])
balance = np.zeros(len(self.floors), dtype=int)
if len(odd_floors) == 0:
return balance.tolist()
shuffle = np.random.permutation(len(odd_floors))[:len(odd_floors) // 2]
odd_floors = odd_floors[shuffle]
balance[odd_floors] = 1
return balance.tolist()
def update_tower(self, keep: np.ndarray, other_discard: np.ndarray):
new_floors = []
for floor in self.floors:
new_floor = np.intersect1d(floor, keep)
if len(new_floor):
new_floors.append(new_floor)
self.floors = new_floors
self.floors.insert(0, other_discard)
def __str__(self):
str_val = ""
floor_numb = [i for i in reversed(range(len(self.floors)))]
for idx, val in enumerate(reversed(self.floors)):
str_val += f"Floor {floor_numb[idx]}: {val.tolist()}\n"
return str_val
def tolist(self) -> list[int]:
tower = []
for floor in self.floors:
tower.extend(floor.tolist())
return tower
@dataclass
class TowerShuffle:
total_positions: int
left_tower: Tower
right_tower: Tower
@classmethod
def new(cls, total_pos:int):
assert total_pos >= 3
rand_pos = np.random.permutation(total_pos)
return TowerShuffle(
total_positions=total_pos,
left_tower=Tower(floors=[rand_pos[:total_pos//2]]),
right_tower=Tower(floors=[rand_pos[total_pos//2:]]),
)
def shuffle(self):
left_keep, left_discard = self.left_tower.split_tower()
right_keep, right_discard = self.right_tower.split_tower()
self.left_tower.update_tower(left_keep, right_discard)
self.right_tower.update_tower(right_keep, left_discard)
def __str__(self):
return f"""Left Tower:
{self.left_tower}
Right Tower:
{self.right_tower}"""

View File

@@ -1,7 +1,66 @@
import random
from math import factorial, comb
from src.evilnkode import Observation
from src.keypad.keypad import BaseKeypad
from typing import Iterator
def total_valid_nkode_states(k: int, p: int) -> int:
return factorial(k) ** (p-1)
return factorial(k) ** (p - 1)
def total_shuffle_states(k: int, p: int) -> int:
return comb((p-1), (p-1) // 2) * factorial(k)
return comb((p - 1), (p - 1) // 2) * factorial(k)
def observations(target_passcode: list[int], keypad: BaseKeypad, number_of_observations: int = 100) -> Iterator[
Observation]:
def obs():
for _ in range(number_of_observations):
yield Observation(
keypad=keypad.keypad_mat(),
key_selection=keypad.key_entry(target_passcode=target_passcode)
)
keypad.shuffle()
return obs()
def passcode_generator(k: int, p: int, n: int, c: int, d: int) -> list[int]:
assert n >= c
assert p * k >= c
assert n >= d
assert p >= d
passcode_prop = []
passcode_set = []
valid_choices = {i for i in range(k * p)}
repeat_set = n - d
repeat_prop = n - c
prop_added = set()
set_added = set()
for _ in range(n):
prop = random.choice(list(valid_choices))
prop_set = prop // p
passcode_prop.append(prop)
passcode_set.append(prop_set)
if prop in prop_added:
repeat_prop -= 1
if prop_set in set_added:
repeat_set -= 1
prop_added.add(prop)
set_added.add(prop_set)
if repeat_prop <= 0:
valid_choices -= prop_added
if repeat_set <= 0:
for el in valid_choices.copy():
if el // p in set_added:
valid_choices.remove(el)
return passcode_prop

View File

@@ -1,6 +1,14 @@
from src.benchmark import passcode_generator
from src.utils import passcode_generator
from src.keypad.keypad import (
RandomShuffleKeypad,
RandomSplitShuffleKeypad,
SlidingTowerShuffleKeypad,
SlidingSplitShuffleKeypad
)
from src.benchmark import benchmark
import pytest
@pytest.mark.parametrize(
"k, p, n, c, d, runs",
[
@@ -10,6 +18,29 @@ import pytest
def test_passcode_generator(k, p, n, c, d, runs):
for _ in range(runs):
passcode = passcode_generator(k=k, p=p, n=n, c=c, d=d)
passcode_sets = [el//p for el in passcode]
assert c <= len(set(passcode))
passcode_sets = [el // p for el in passcode]
assert c <= len(set(passcode))
assert d <= len(set(passcode_sets))
@pytest.mark.parametrize(
"number_of_keys,properties_per_key,passcode_len,max_tries_before_lockout,complexity,disparity,run_count,keypad",
[
(6, 8, 4, 5, 4, 4, 100, RandomShuffleKeypad),
(6, 8, 4, 5, 4, 4, 100, RandomSplitShuffleKeypad),
(6, 8, 4, 5, 4, 4, 100, SlidingSplitShuffleKeypad),
(6, 8, 4, 5, 4, 4, 100, SlidingTowerShuffleKeypad),
]
)
def test_benchmark(number_of_keys, properties_per_key, passcode_len, max_tries_before_lockout, complexity, disparity,
run_count, keypad):
benchmark(
number_of_keys=number_of_keys,
properties_per_key=properties_per_key,
passcode_len=passcode_len,
max_tries_before_lockout=max_tries_before_lockout,
run_count=run_count,
complexity=complexity,
disparity=disparity,
keypad=keypad.new_keypad(number_of_keys, properties_per_key)
)

View File

@@ -1,45 +1,34 @@
import random
import pytest
from src.evilkode import Evilkode, Observation
from src.keypad import Keypad
from src.evilnkode import EvilNKode
from src.keypad.keypad import (
RandomShuffleKeypad,
)
from src.utils import observations
@pytest.fixture
def observations(number_of_keys, properties_per_key, passcode_len):
k = number_of_keys
p = properties_per_key
n = passcode_len
nkode = [random.randint(0, k*p-1) for _ in range(n)]
keypad = Keypad.new_keypad(k, p)
def obs_gen():
for _ in range(100): # finite number of yields
yield Observation(
keypad=keypad.keypad.copy(),
key_selection=keypad.key_entry(target_passcode=nkode)
)
keypad.split_shuffle()
return obs_gen()
def passcode(number_of_keys, properties_per_key, passcode_len):
return [random.randint(0, number_of_keys * properties_per_key - 1) for _ in range(passcode_len)]
@pytest.mark.parametrize(
"number_of_keys, properties_per_key, passcode_len",
[
(5, 3, 4), # Test case 1
(5, 4, 4), # Test case 1
(10, 5, 6), # Test case 2
(8, 4, 5), # Test case 3
(8, 4, 5), # Test case 3
]
)
def test_evilkode(number_of_keys, properties_per_key, passcode_len, observations):
evilkode = Evilkode(
observations=observations,
def test_evilkode(number_of_keys, properties_per_key, passcode_len, passcode):
keypad = RandomShuffleKeypad.new_keypad(number_of_keys, properties_per_key)
obs = observations(passcode, keypad)
evilkode = EvilNKode(
observations=obs,
number_of_keys=number_of_keys,
properties_per_key=properties_per_key,
passcode_len=passcode_len,
)
evilout = evilkode.run()
assert evilout.iterations > 1
assert evilout.iterations_to_break > 1

View File

@@ -1,31 +1,34 @@
import pytest
import numpy as np
from src.keypad.keypad import (
RandomSplitShuffleKeypad,
RandomShuffleKeypad,
SlidingSplitShuffleKeypad,
SlidingTowerShuffleKeypad,
)
from src.keypad import Keypad
def test_keypad():
keypad = Keypad(
def test_key_entry():
keypad = RandomShuffleKeypad(
keypad=np.array([
[8, 9, 10, 11],
[0, 5, 2, 3],
[4, 1, 6,7]
]), k= 3, p=4, keypad_cache=[])
[4, 1, 6, 7]
]), k=3, p=4)
assert keypad.key_entry([8, 5, 6, 11]) == [0, 1, 2, 0]
assert keypad.key_entry([8, 5, 6, 11]) == [0,1,2,0]
def test_split_shuffle():
p = 4 # properties_per_key
k = 3 # number_of_keys
keypad = Keypad.new_keypad(k, p)
@pytest.mark.parametrize(
"keypad_type, number_of_keys, properties_per_key",
[
(RandomShuffleKeypad, 3, 4),
(RandomSplitShuffleKeypad, 3, 4),
(SlidingTowerShuffleKeypad, 3, 4),
(SlidingSplitShuffleKeypad, 3, 4),
]
)
def test_keypad_shuffle(keypad_type, number_of_keys, properties_per_key):
keypad = keypad_type.new_keypad(number_of_keys, properties_per_key)
print(keypad.keypad)
keypad.split_shuffle()
keypad.shuffle()
print(keypad.keypad)
def test_full_shuffle():
p = 4 # properties_per_key
k = 3 # number_of_keys
keypad = Keypad.new_keypad(k, p)
print(keypad.keypad)
keypad.full_shuffle()
print(keypad.keypad)

View File

@@ -0,0 +1,8 @@
from src.keypad.tower_shuffle import TowerShuffle
def test_tower_shuffle():
tower = TowerShuffle.new(9)
print(tower)
for _ in range(100):
tower.shuffle()
print(tower)