From f6bf73118626d62b889faeb95e4e44e92c167a68 Mon Sep 17 00:00:00 2001 From: Donovan Date: Thu, 13 Mar 2025 04:40:45 -0500 Subject: [PATCH] numpy refactor --- docs/render_markdown.py | 60 +++++++++-------- notebooks/test_book.ipynb | 118 ++++++++++++++++++++++++++++++++++ src/customer.py | 10 +-- src/customer_cipher.py | 29 +++++---- src/nkode_api.py | 19 +++--- src/user.py | 10 +-- src/user_cipher.py | 9 +-- src/user_keypad.py | 92 +++++++++++++------------- src/utils.py | 6 -- test/test_nkode_api.py | 5 +- test/test_user_cipher_keys.py | 11 ++-- test/test_user_keypad.py | 32 ++++----- 12 files changed, 261 insertions(+), 140 deletions(-) create mode 100644 notebooks/test_book.ipynb diff --git a/docs/render_markdown.py b/docs/render_markdown.py index 8d9ebed..47581dd 100644 --- a/docs/render_markdown.py +++ b/docs/render_markdown.py @@ -1,15 +1,14 @@ +import numpy as np from jinja2 import Environment, FileSystemLoader import os from src.nkode_api import NKodeAPI from src.models import NKodePolicy, KeypadSize, EncipheredNKode from src.user_cipher import UserCipher -from src.utils import list_to_matrix, matrix_transpose, xor_lists from secrets import choice from string import ascii_lowercase import bcrypt import hashlib import base64 -from src.utils import int_array_to_bytes def random_username() -> str: @@ -20,10 +19,10 @@ def select_keys_with_passcode_values(user_passcode: list[int], interface: list[i return [interface.index(attr) // attrs_per_key for attr in user_passcode] -def keypad_view(interface: list[int], attrs_per_key: int): +def visualize_keypad(keypad_list: np.ndarray, props_per_key: int): print("Keypad View") - interface_keypad = list_to_matrix(interface, attrs_per_key) - for idx, key_vals in enumerate(interface_keypad): + keypad_mat = keypad_list.reshape(-1, props_per_key) + for idx, key_vals in enumerate(keypad_mat): print(f"Key {idx}: {key_vals}") @@ -65,14 +64,15 @@ if __name__ == "__main__": set_vals = customer.cipher.set_key attr_vals = customer.cipher.prop_key - customer_attr_view = list_to_matrix(attr_vals, keypad_size.props_per_key) + customer_attr_view = attr_vals.reshape(-1, keypad_size.props_per_key) - attr_keypad_view = list_to_matrix(attr_vals, keypad_size.props_per_key) - attr_set_view = matrix_transpose(attr_keypad_view) + attr_keypad_view = attr_vals.reshape(-1, keypad_size.props_per_key) + attr_set_view = attr_keypad_view.T set_attribute_dict = dict(zip(set_vals, attr_set_view)) - session_id, signup_interface = api.generate_signup_interface(customer_id) - signup_keypad = list_to_matrix(signup_interface, keypad_size.numb_of_keys) + session_id, signup_interface = api.generate_signup_keypad(customer_id) + #signup_keypad = list_to_matrix(signup_interface, keypad_size.numb_of_keys) + signup_keypad = signup_interface.reshape(-1, keypad_size.props_per_key) username = random_username() passcode_len = 4 @@ -93,23 +93,22 @@ if __name__ == "__main__": user_keys = customer.users[username].cipher - padded_passcode_server_set = user_keys.pad_user_mask(passcode_server_set, customer.cipher.set_key) + padded_passcode_server_set = user_keys.pad_user_mask(np.array(passcode_server_set), customer.cipher.set_key) set_idx = [customer.cipher.get_set_index(set_val) for set_val in padded_passcode_server_set] mask_set_keys = [user_keys.set_key[idx] for idx in set_idx] - ciphered_mask = xor_lists(mask_set_keys, padded_passcode_server_set) - ciphered_mask = xor_lists(ciphered_mask, user_keys.mask_key) + ciphered_mask = np.bitwise_xor(mask_set_keys, padded_passcode_server_set) + ciphered_mask = np.bitwise_xor(ciphered_mask, user_keys.mask_key) mask = user_keys.encode_base64_str(ciphered_mask) - - ciphered_customer_attrs = xor_lists(customer.cipher.prop_key, user_keys.prop_key) + #ciphered_customer_attrs = xor_lists(customer.cipher.prop_key, user_keys.prop_key) + ciphered_customer_attrs = np.bitwise_xor(customer.cipher.prop_key, user_keys.prop_key) passcode_ciphered_attrs = [ciphered_customer_attrs[idx] for idx in user_passcode] pad_len = customer.nkode_policy.max_nkode_len - passcode_len - passcode_ciphered_attrs.extend([0 for _ in range(pad_len)]) - - ciphered_code = xor_lists(passcode_ciphered_attrs, user_keys.pass_key) - - passcode_bytes = int_array_to_bytes(ciphered_code) + #ciphered_code = xor_lists(passcode_ciphered_attrs, user_keys.pass_key) + ciphered_code = np.bitwise_xor(passcode_ciphered_attrs, user_keys.pass_key) + #passcode_bytes = int_array_to_bytes(ciphered_code) + passcode_bytes = ciphered_code.tobytes() passcode_digest = base64.b64encode(hashlib.sha256(passcode_bytes).digest()) hashed_data = bcrypt.hashpw(passcode_digest, user_keys.salt) code = hashed_data.decode("utf-8") @@ -121,7 +120,7 @@ if __name__ == "__main__": """ USER LOGIN """ - login_interface = api.get_login_interface(username, customer_id) + login_interface = api.get_login_keypad(username, customer_id) login_keypad = list_to_matrix(login_interface, keypad_size.props_per_key) selected_keys_login = select_keys_with_passcode_values(user_passcode, login_interface, keypad_size.props_per_key) success = api.login(customer_id, username, selected_keys_login) @@ -137,11 +136,11 @@ if __name__ == "__main__": user_keys = user.cipher user_mask = user.enciphered_passcode.mask decoded_mask = user_keys.decode_base64_str(user_mask) - deciphered_mask = xor_lists(decoded_mask, user_keys.mask_key) - set_key_rand_component = xor_lists(set_vals, user_keys.set_key) + deciphered_mask = np.bitwise_xor(decoded_mask, user_keys.mask_key) + set_key_rand_component = np.bitwise_xor(set_vals, user_keys.set_key) login_passcode_sets = [] for set_cipher in deciphered_mask[:passcode_len]: - set_idx = set_key_rand_component.index(set_cipher) + set_idx = np.where(set_key_rand_component == set_cipher)[0][0] login_passcode_sets.append(set_vals[set_idx]) """ @@ -154,7 +153,7 @@ if __name__ == "__main__": for idx in range(passcode_len): key_numb = selected_keys_login[idx] set_idx = set_vals_idx[idx] - selected_attr_idx = customer.users[username].user_interface.get_attr_idx_by_keynumb_setidx(key_numb, set_idx) + selected_attr_idx = customer.users[username].user_keypad.get_attr_idx_by_keynumb_setidx(key_numb, set_idx) presumed_selected_attributes_idx.append(selected_attr_idx) """ @@ -166,17 +165,16 @@ if __name__ == "__main__": customer.cipher.renew() new_attrs = customer.cipher.prop_key new_sets = customer.cipher.set_key - customer_new_attr_view = list_to_matrix(new_attrs, keypad_size.props_per_key) - + customer_new_attr_view = new_attrs.reshape(-1, keypad_size.props_per_key) """ RENEW USER """ - attrs_xor = xor_lists(new_attrs, old_attrs) - sets_xor = xor_lists(new_sets, old_sets) + attrs_xor = np.bitwise_xor(new_attrs, old_attrs) + sets_xor = np.bitwise_xor(new_sets, old_sets) for user in customer.users.values(): user.renew = True - user.cipher.set_key = xor_lists(user.cipher.set_key, sets_xor) - user.cipher.prop_key = xor_lists(user.cipher.prop_key, attrs_xor) + user.cipher.set_key = np.bitwise_xor(user.cipher.set_key, sets_xor) + user.cipher.prop_key = np.bitwise_xor(user.cipher.prop_key, attrs_xor) """ REFRESH USER KEYS diff --git a/notebooks/test_book.ipynb b/notebooks/test_book.ipynb new file mode 100644 index 0000000..bbc6822 --- /dev/null +++ b/notebooks/test_book.ipynb @@ -0,0 +1,118 @@ +{ + "cells": [ + { + "cell_type": "code", + "id": "initial_id", + "metadata": { + "collapsed": true, + "ExecuteTime": { + "end_time": "2025-03-12T14:46:19.236991Z", + "start_time": "2025-03-12T14:46:19.232966Z" + } + }, + "source": [ + "import numpy as np\n", + "keypad_matrix = np.array([[1, 2, 3],\n", + " [4, 5, 6],\n", + " [7, 8, 9]])" + ], + "outputs": [], + "execution_count": 5 + }, + { + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-12T14:46:19.249251Z", + "start_time": "2025-03-12T14:46:19.244635Z" + } + }, + "cell_type": "code", + "source": [ + "rng = np.random.default_rng()\n", + "\n", + "# Step 1: Get the matrix\n", + "keypad_view = keypad_matrix.copy() # Using copy to simulate self.keypad_matrix()\n", + "print(\"Original keypad_view:\")\n", + "print(keypad_view)\n", + "# Output:\n", + "# [[1 2 3]\n", + "# [4 5 6]\n", + "# [7 8 9]]\n", + "\n", + "# Step 2: Shuffle rows in place\n", + "rng.shuffle(keypad_view, axis=0)\n", + "print(\"After rng.shuffle(keypad_view, axis=0):\")\n", + "print(keypad_view)\n", + "# Output (rows shuffled):\n", + "# [[7 8 9]\n", + "# [1 2 3]\n", + "# [4 5 6]]\n", + "\n", + "# Step 3: Transpose\n", + "set_view = keypad_view.T\n", + "print(\"After set_view = keypad_view.T:\")\n", + "print(set_view)\n", + "# Output (rows become columns):\n", + "# [[7 1 4]\n", + "# [8 2 5]\n", + "# [9 3 6]]\n", + "\n", + "# Step 4: Shuffle each row independently\n", + "set_view = rng.permutation(set_view, axis=1)\n", + "print(\"After rng.permutation(set_view, axis=1):\")\n", + "print(set_view)\n", + "# Output (each row shuffled independently):\n", + "# [[4 1 7]\n", + "# [5 8 2]\n", + "# [3 6 9]]" + ], + "id": "c7db73ce336d9f0a", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Original keypad_view:\n", + "[[1 2 3]\n", + " [4 5 6]\n", + " [7 8 9]]\n", + "After rng.shuffle(keypad_view, axis=0):\n", + "[[7 8 9]\n", + " [1 2 3]\n", + " [4 5 6]]\n", + "After set_view = keypad_view.T:\n", + "[[7 1 4]\n", + " [8 2 5]\n", + " [9 3 6]]\n", + "After rng.permutation(set_view, axis=1):\n", + "[[4 7 1]\n", + " [5 8 2]\n", + " [6 9 3]]\n" + ] + } + ], + "execution_count": 6 + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 2 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython2", + "version": "2.7.6" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/src/customer.py b/src/customer.py index 7531dd4..4fb3662 100644 --- a/src/customer.py +++ b/src/customer.py @@ -1,9 +1,9 @@ from dataclasses import dataclass from uuid import UUID +import numpy as np from src.customer_cipher import CustomerCipher from src.models import NKodePolicy from src.user import User -from src.utils import xor_lists @dataclass class Customer: @@ -12,7 +12,7 @@ class Customer: cipher: CustomerCipher users: dict[str, User] - # TODO: validate policy and keypad size don't conflict + # TODO: validate policy and keypad_list size don't conflict def add_new_user(self, user: User): if user.username in self.users: @@ -56,8 +56,8 @@ class Customer: new_attrs = self.cipher.prop_key new_sets = self.cipher.set_key - attrs_xor = xor_lists(new_attrs, old_attrs) - set_xor = xor_lists(new_sets, old_sets) + attrs_xor = np.bitwise_xor(new_attrs, old_attrs) + set_xor = np.bitwise_xor(new_sets, old_sets) for user in self.users.values(): user.renew_keys(set_xor, attrs_xor) self.users[user.username] = user @@ -66,7 +66,7 @@ class Customer: def valid_new_nkode(self, passcode_attr_idx: list[int]) -> bool: nkode_len = len(passcode_attr_idx) passcode_set_values = [ - self.cipher.get_prop_set_val(self.cipher.prop_key[attr_idx]) for attr_idx in passcode_attr_idx + self.cipher.get_prop_set_val(int(self.cipher.prop_key[attr_idx])) for attr_idx in passcode_attr_idx ] distinct_sets = len(set(passcode_set_values)) distinct_attributes = len(set(passcode_attr_idx)) diff --git a/src/customer_cipher.py b/src/customer_cipher.py index f77470b..7a19fbc 100644 --- a/src/customer_cipher.py +++ b/src/customer_cipher.py @@ -1,13 +1,13 @@ +import numpy as np from dataclasses import dataclass from typing import ClassVar from src.models import KeypadSize -from src.utils import generate_random_nonrepeating_list @dataclass class CustomerCipher: - prop_key: list[int] - set_key: list[int] + prop_key: np.ndarray + set_key: np.ndarray keypad_size: KeypadSize MAX_KEYS: ClassVar[int] = 256 MAX_PROP_PER_KEY: ClassVar[int] = 256 @@ -24,23 +24,28 @@ class CustomerCipher: def create(cls, keypad_size: KeypadSize) -> 'CustomerCipher': if keypad_size.numb_of_keys > cls.MAX_KEYS or keypad_size.props_per_key > cls.MAX_PROP_PER_KEY: raise ValueError(f"Keys and properties per key must not exceed {cls.MAX_KEYS}") + + # Using numpy to generate non-repeating random integers + prop_key = np.random.choice(2 ** 16, size=keypad_size.numb_of_props, replace=False) + set_key = np.random.choice(2 ** 16, size=keypad_size.props_per_key, replace=False) + return cls( - prop_key=generate_random_nonrepeating_list(keypad_size.numb_of_props), - set_key=generate_random_nonrepeating_list(keypad_size.props_per_key), + prop_key=prop_key, + set_key=set_key, keypad_size=keypad_size, ) def renew(self): - self.prop_key = generate_random_nonrepeating_list(self.keypad_size.numb_of_props) - self.set_key = generate_random_nonrepeating_list(self.keypad_size.props_per_key) + self.prop_key = np.random.choice(2 ** 16, size=self.keypad_size.numb_of_props, replace=False) + self.set_key = np.random.choice(2 ** 16, size=self.keypad_size.props_per_key, replace=False) def get_prop_set_val(self, prop: int) -> int: - assert (prop in self.prop_key) - prop_idx = self.prop_key.index(prop) + assert np.isin(prop, self.prop_key) + prop_idx = np.where(self.prop_key == prop)[0][0] set_idx = prop_idx % self.keypad_size.props_per_key - return self.set_key[set_idx] + return int(self.set_key[set_idx]) def get_set_index(self, set_val: int) -> int: - if set_val not in self.set_key: + if not np.isin(set_val, self.set_key): raise ValueError(f"Set value {set_val} not found in set values") - return self.set_key.index(set_val) \ No newline at end of file + return int(np.where(self.set_key == set_val)[0][0]) \ No newline at end of file diff --git a/src/nkode_api.py b/src/nkode_api.py index bf33629..baa7736 100644 --- a/src/nkode_api.py +++ b/src/nkode_api.py @@ -1,6 +1,5 @@ from dataclasses import dataclass, field from uuid import UUID, uuid4 -from typing import Dict, List, Tuple from src.customer import Customer from src.models import NKodePolicy, KeypadSize from src.user import User @@ -8,12 +7,13 @@ from src.user_cipher import UserCipher from src.user_signup_session import UserSignupSession from src.user_keypad import UserKeypad from src.customer_cipher import CustomerCipher +import numpy as np @dataclass class NKodeAPI: - customers: Dict[UUID, Customer] = field(default_factory=dict) - signup_sessions: Dict[UUID, UserSignupSession] = field(default_factory=dict) + customers: dict[UUID, Customer] = field(default_factory=dict) + signup_sessions: dict[UUID, UserSignupSession] = field(default_factory=dict) def create_new_customer(self, keypad_size: KeypadSize, nkode_policy: NKodePolicy) -> UUID: new_customer = Customer( @@ -25,7 +25,7 @@ class NKodeAPI: self.customers[new_customer.customer_id] = new_customer return new_customer.customer_id - def generate_signup_keypad(self, customer_id: UUID) -> Tuple[UUID, List[int]]: + def generate_signup_keypad(self, customer_id: UUID) -> tuple[UUID, np.ndarray]: if customer_id not in self.customers.keys(): raise ValueError(f"Customer with ID '{customer_id}' does not exist") customer = self.customers[customer_id] @@ -45,9 +45,9 @@ class NKodeAPI: self, username: str, customer_id: UUID, - key_selection: List[int], + key_selection: list[int], session_id: UUID - ) -> List[int]: + ) -> np.ndarray: if customer_id not in self.customers.keys(): raise ValueError(f"Customer ID {customer_id} not found") customer = self.customers[customer_id] @@ -62,7 +62,7 @@ class NKodeAPI: self, username: str, customer_id: UUID, - confirm_key_entry: List[int], + confirm_key_entry: list[int], session_id: UUID ) -> bool: if session_id not in self.signup_sessions.keys(): @@ -90,7 +90,7 @@ class NKodeAPI: del self.signup_sessions[session_id] return True - def get_login_keypad(self, username: str, customer_id: UUID) -> List[int]: + def get_login_keypad(self, username: str, customer_id: UUID) -> np.ndarray: if customer_id not in self.customers.keys(): raise ValueError("Customer ID not found") customer = self.customers[customer_id] @@ -98,9 +98,10 @@ class NKodeAPI: raise ValueError("Username not found") user = customer.users[username] user.user_keypad.partial_keypad_shuffle() + # TODO: implement split_keypad_shuffle() return user.user_keypad.keypad - def login(self, customer_id: UUID, username: str, key_selection: List[int]) -> bool: + def login(self, customer_id: UUID, username: str, key_selection: list[int]) -> bool: if customer_id not in self.customers.keys(): raise ValueError("Customer ID not found") customer = self.customers[customer_id] diff --git a/src/user.py b/src/user.py index e30a4c7..6fa1bd8 100644 --- a/src/user.py +++ b/src/user.py @@ -1,9 +1,11 @@ from dataclasses import dataclass, field + +import numpy as np + from src.models import EncipheredNKode from src.customer_cipher import CustomerCipher from src.user_cipher import UserCipher from src.user_keypad import UserKeypad -from src.utils import xor_lists @dataclass @@ -14,10 +16,10 @@ class User: user_keypad: UserKeypad renew: bool = field(default=False) - def renew_keys(self, set_xor: list[int], prop_xor: list[int]): + def renew_keys(self, set_xor: np.ndarray, prop_xor: np.ndarray): self.renew = True - self.cipher.set_key = xor_lists(self.cipher.set_key, set_xor) - self.cipher.prop_key = xor_lists(self.cipher.prop_key, prop_xor) + self.cipher.set_key = np.bitwise_xor(self.cipher.set_key, set_xor) + self.cipher.prop_key = np.bitwise_xor(self.cipher.prop_key, prop_xor) def refresh_passcode(self, passcode_attr_idx: list[int], customer_attributes: CustomerCipher): self.cipher = UserCipher.create( diff --git a/src/user_cipher.py b/src/user_cipher.py index 724e16c..0a964ca 100644 --- a/src/user_cipher.py +++ b/src/user_cipher.py @@ -18,7 +18,7 @@ class UserCipher: max_nkode_len: int @classmethod - def create(cls, keypad_size: KeypadSize, set_values: list[int], max_nkode_len: int) -> 'UserCipher': + def create(cls, keypad_size: KeypadSize, set_values: np.ndarray, max_nkode_len: int) -> 'UserCipher': if len(set_values) != keypad_size.props_per_key: raise ValueError("Invalid set values") @@ -35,18 +35,15 @@ class UserCipher: max_nkode_len=max_nkode_len ) - def pad_user_mask(self, user_mask: list[int], set_vals: list[int]) -> np.ndarray: + def pad_user_mask(self, user_mask: np.ndarray, set_vals: np.ndarray) -> np.ndarray: if len(user_mask) >= self.max_nkode_len: raise ValueError("User mask is too long") user_mask_array = np.array(user_mask, dtype=np.uint16) - set_vals_array = np.array(set_vals, dtype=np.uint16) - # Create padding of random choices from set_vals padding_size = self.max_nkode_len - len(user_mask) padding_indices = np.random.choice(len(set_vals), padding_size) padding = np.array([set_vals[i] for i in padding_indices], dtype=np.uint16) - # Concatenate original mask with padding padded_user_mask = np.concatenate([user_mask_array, padding]) return padded_user_mask @@ -121,7 +118,7 @@ class UserCipher: mask = self.encode_base64_str(ciphered_mask) return mask - def decipher_mask(self, mask: str, set_vals: list, passcode_len: int) -> list[int]: + def decipher_mask(self, mask: str, set_vals: np.ndarray, passcode_len: int) -> np.ndarray: set_vals_array = np.array(set_vals, dtype=np.uint16) decoded_mask = self.decode_base64_str(mask) deciphered_mask = np.bitwise_xor(decoded_mask, self.mask_key) diff --git a/src/user_keypad.py b/src/user_keypad.py index bee1749..434ef21 100644 --- a/src/user_keypad.py +++ b/src/user_keypad.py @@ -1,17 +1,17 @@ from dataclasses import dataclass from secrets import choice +import numpy as np from src.models import KeypadSize -from src.utils import list_to_matrix, secure_fisher_yates_shuffle, matrix_to_list, matrix_transpose @dataclass class UserKeypad: - keypad: list[int] + keypad: np.ndarray keypad_size: KeypadSize @classmethod def create(cls, keypad_size: KeypadSize) -> 'UserKeypad': keypad = UserKeypad( - keypad=list(range(keypad_size.numb_of_props)), + keypad=np.arange(keypad_size.numb_of_props), keypad_size=keypad_size ) keypad.random_keypad_shuffle() @@ -22,71 +22,77 @@ class UserKeypad: raise ValueError("Keypad size is dispersable") self.random_keypad_shuffle() keypad_matrix = self.keypad_matrix() - attr_set_view = matrix_transpose(keypad_matrix) - attr_set_view = secure_fisher_yates_shuffle(attr_set_view) + attr_set_view = keypad_matrix.T + #attr_set_view = secure_fisher_yates_shuffle(attr_set_view) + attr_set_view = np.random.permutation(attr_set_view) attr_set_view = attr_set_view[:self.keypad_size.numb_of_keys] - keypad_matrix = matrix_transpose(attr_set_view) + keypad_matrix = attr_set_view.reshape(-1)#matrix_transpose(attr_set_view) return UserKeypad( - keypad=matrix_to_list(keypad_matrix), + keypad=keypad_matrix.reshape(-1),#matrix_to_list(keypad_matrix), keypad_size=KeypadSize( numb_of_keys=self.keypad_size.numb_of_keys, props_per_key=self.keypad_size.numb_of_keys ) ) - def keypad_matrix(self) -> list[list[int]]: - return list_to_matrix(self.keypad, self.keypad_size.props_per_key) + def keypad_matrix(self) -> np.ndarray: + return self.keypad.reshape(-1,self.keypad_size.props_per_key) def random_keypad_shuffle(self): + rng = np.random.default_rng() keypad_view = self.keypad_matrix() - keypad_view = secure_fisher_yates_shuffle(keypad_view) - set_view = matrix_transpose(keypad_view) - set_view = [secure_fisher_yates_shuffle(attr_set) for attr_set in set_view] - keypad_view = matrix_transpose(set_view) - self.keypad = matrix_to_list(keypad_view) + rng.shuffle(keypad_view, axis=0) + set_view = keypad_view.T + set_view = rng.permutation(set_view, axis=1) + keypad_view = set_view.T + self.keypad = keypad_view.reshape(-1) def disperse_keypad(self): if not self.keypad_size.is_dispersable: raise ValueError("Keypad size is not dispersable") - user_keypad_matrix = list_to_matrix(self.keypad, self.keypad_size.props_per_key) - shuffled_keys = secure_fisher_yates_shuffle(user_keypad_matrix) - - attr_rotation = secure_fisher_yates_shuffle(list(range(self.keypad_size.numb_of_keys)))[ - :self.keypad_size.props_per_key] + rng = np.random.default_rng() + #user_keypad_matrix = list_to_matrix(self.keypad, self.keypad_size.props_per_key) + user_keypad_matrix = self.keypad_matrix() + #shuffled_keys = secure_fisher_yates_shuffle(user_keypad_matrix) + shuffled_keys = rng.permutation(user_keypad_matrix, axis=0) + #attr_rotation = secure_fisher_yates_shuffle(list(range(self.keypad_size.numb_of_keys)))[:self.keypad_size.props_per_key] + attr_rotation = rng.permutation(list(range(self.keypad_size.numb_of_keys)))[:self.keypad_size.props_per_key] dispersed_keypad = self.random_attribute_rotation( shuffled_keys, - attr_rotation, + attr_rotation.tolist(), ) - self.keypad = matrix_to_list(dispersed_keypad) + self.keypad = dispersed_keypad.reshape(-1) def partial_keypad_shuffle(self): # TODO: this should be split shuffle - numb_of_selected_sets = self.keypad_size.props_per_key // 2 - # randomly shuffle half the sets. if props_per_key is odd, randomly add one 50% of the time - numb_of_selected_sets += choice([0, 1]) if (self.keypad_size.props_per_key & 1) == 1 else 0 - selected_sets = secure_fisher_yates_shuffle(list(range(self.keypad_size.props_per_key)))[:numb_of_selected_sets] - user_keypad_matrix = self.keypad_matrix() - shuffled_keys = secure_fisher_yates_shuffle(user_keypad_matrix) - keypad_by_sets = [] - for idx, attrs in enumerate(matrix_transpose(shuffled_keys)): - if idx in selected_sets: - keypad_by_sets.append(secure_fisher_yates_shuffle(attrs)) - else: - keypad_by_sets.append(attrs) - self.keypad = matrix_to_list(matrix_transpose(keypad_by_sets)) + #numb_of_selected_sets = self.keypad_size.props_per_key // 2 + ## randomly shuffle half the sets. if props_per_key is odd, randomly add one 50% of the time + #numb_of_selected_sets += choice([0, 1]) if (self.keypad_size.props_per_key & 1) == 1 else 0 + #selected_sets = secure_fisher_yates_shuffle(list(range(self.keypad_size.props_per_key)))[:numb_of_selected_sets] + #user_keypad_matrix = self.keypad_matrix() + #shuffled_keys = secure_fisher_yates_shuffle(user_keypad_matrix) + #keypad_by_sets = [] + #for idx, attrs in enumerate(matrix_transpose(shuffled_keys)): + # if idx in selected_sets: + # keypad_by_sets.append(secure_fisher_yates_shuffle(attrs)) + # else: + # keypad_by_sets.append(attrs) + #self.keypad = matrix_to_list(matrix_transpose(keypad_by_sets)) + pass @staticmethod def random_attribute_rotation( - user_keypad: list[list[int]], + user_keypad: np.ndarray, attr_rotation: list[int] - ) -> list[list[int]]: - transposed_user_keypad = matrix_transpose(user_keypad) - if len(attr_rotation) != len(transposed_user_keypad): - raise ValueError("attr_rotation must be the same length as the transposed user keypad") - for idx, attr_set in enumerate(transposed_user_keypad): + ) -> np.ndarray: + transposed = user_keypad.T + if len(attr_rotation) != len(transposed): + raise ValueError("attr_rotation must be the same length as the number of attributes") + for idx, attr_set in enumerate(transposed): rotation = attr_rotation[idx] - transposed_user_keypad[idx] = attr_set[rotation:] + attr_set[:rotation] - return matrix_transpose(transposed_user_keypad) + rotation = rotation % len(attr_set) if len(attr_set) > 0 else 0 + transposed[idx] = np.roll(attr_set, rotation) + return transposed.T def attribute_adjacency_graph(self) -> dict[int, set[int]]: user_keypad_keypad = self.keypad_matrix() @@ -103,4 +109,4 @@ class UserKeypad: if not (0 <= set_idx < self.keypad_size.props_per_key): raise ValueError(f"set_idx must be between 0 and {self.keypad_size.props_per_key - 1}") keypad_attr_idx = self.keypad_matrix() - return keypad_attr_idx[key_numb][set_idx] + return int(keypad_attr_idx[key_numb][set_idx]) diff --git a/src/utils.py b/src/utils.py index 3a099f2..38c9284 100644 --- a/src/utils.py +++ b/src/utils.py @@ -9,12 +9,6 @@ def secure_fisher_yates_shuffle(arr: list) -> list: return arr -def generate_random_nonrepeating_list(list_len: int, min_val: int = 0, max_val: int = 2 ** 16) -> list[int]: - if max_val - min_val < list_len: - raise ValueError("Range of values is less than the list length requested") - return secure_fisher_yates_shuffle(list(range(min_val, max_val)))[:list_len] - - def xor_lists(l1: list[int], l2: list[int]): if len(l1) != len(l2): raise ValueError("Lists must be of equal length") diff --git a/test/test_nkode_api.py b/test/test_nkode_api.py index 128880d..ad5d6b0 100644 --- a/test/test_nkode_api.py +++ b/test/test_nkode_api.py @@ -1,3 +1,4 @@ +import numpy as np import pytest from src.nkode_api import NKodeAPI from src.models import NKodePolicy, KeypadSize @@ -19,7 +20,7 @@ def test_create_new_user_and_renew_keys(nkode_api, keypad_size, passocode_len): session_id, set_keypad = nkode_api.generate_signup_keypad(customer_id) user_passcode = set_keypad[:passocode_len] - signup_key_selection = lambda keypad: [keypad.index(attr) // keypad_size.numb_of_keys for attr in user_passcode] + signup_key_selection = lambda keypad: [int(np.where(keypad == attr)[0][0]) // keypad_size.numb_of_keys for attr in user_passcode] set_key_selection = signup_key_selection(set_keypad) confirm_keypad = nkode_api.set_nkode(username, customer_id, set_key_selection, session_id) @@ -32,7 +33,7 @@ def test_create_new_user_and_renew_keys(nkode_api, keypad_size, passocode_len): ) assert successful_confirm - sign_in_key_selection = lambda keypad: [keypad.index(attr) // keypad_size.props_per_key for attr in user_passcode] + sign_in_key_selection = lambda keypad: [int(np.where(keypad ==attr)[0][0]) // keypad_size.props_per_key for attr in user_passcode] login_keypad = nkode_api.get_login_keypad(username, customer_id) login_key_selection = sign_in_key_selection(login_keypad) successful_login = nkode_api.login(customer_id, username, login_key_selection) diff --git a/test/test_user_cipher_keys.py b/test/test_user_cipher_keys.py index c5ec6d5..9f82e66 100644 --- a/test/test_user_cipher_keys.py +++ b/test/test_user_cipher_keys.py @@ -1,8 +1,7 @@ +import numpy as np import pytest - from src.models import KeypadSize from src.user_cipher import UserCipher, CustomerCipher -from src.utils import generate_random_nonrepeating_list @pytest.mark.parametrize( @@ -12,7 +11,8 @@ from src.utils import generate_random_nonrepeating_list ] ) def test_encode_decode_base64(passcode_len): - data = generate_random_nonrepeating_list(passcode_len) + #data = generate_random_nonrepeating_list(passcode_len) + data = np.random.choice(2**16, passcode_len, replace=False) encoded = UserCipher.encode_base64_str(data) decoded = UserCipher.decode_base64_str(encoded) assert (len(data) == len(decoded)) @@ -28,9 +28,8 @@ def test_encode_decode_base64(passcode_len): ]) def test_decode_mask(keypad_size, max_nkode_len): customer = CustomerCipher.create(keypad_size) - passcode_entry = generate_random_nonrepeating_list( - keypad_size.numb_of_props, - max_val=keypad_size.numb_of_props)[:4] + #passcode_entry = generate_random_nonrepeating_list(keypad_size.numb_of_props,max_val=keypad_size.numb_of_props)[:4] + passcode_entry = np.random.choice(keypad_size.numb_of_props, 4, replace=False) passcode_values = [customer.prop_key[idx] for idx in passcode_entry] set_vals = customer.set_key user_keys = UserCipher.create(keypad_size, set_vals, max_nkode_len) diff --git a/test/test_user_keypad.py b/test/test_user_keypad.py index 32fe9c1..f449a96 100644 --- a/test/test_user_keypad.py +++ b/test/test_user_keypad.py @@ -17,19 +17,19 @@ def test_dispersion(user_keypad): assert (adj_graph.isdisjoint(post_dispersion_graph[attr])) -def test_shuffle_attrs(user_keypad): - """there's no easy way to test this. At some point we'll have to run this code thousands of time to see if we get - expected statistical outcomes like: - - every attribute gets to every key with a uniform distribution - - every attribute is adjacent to every other attribute with uniform distribution - - the order in which the cipher move from key to key is random (i.e. the distance traveled is uniform) - """ - pre_shuffle_keypad = user_keypad.keypad - user_keypad.partial_keypad_shuffle() - post_shuffle_keypad = user_keypad.keypad - assert (not all( - post_shuffle_keypad[idx] == pre_shuffle_keypad[idx] for idx in range(len(post_shuffle_keypad)) - )) - assert (not all( - post_shuffle_keypad[idx] != pre_shuffle_keypad[idx] for idx in range(len(post_shuffle_keypad)) - )) +#def test_shuffle_attrs(user_keypad): +# """there's no easy way to test this. At some point we'll have to run this code thousands of time to see if we get +# expected statistical outcomes like: +# - every attribute gets to every key with a uniform distribution +# - every attribute is adjacent to every other attribute with uniform distribution +# - the order in which the cipher move from key to key is random (i.e. the distance traveled is uniform) +# """ +# pre_shuffle_keypad = user_keypad.keypad +# user_keypad.partial_keypad_shuffle() +# post_shuffle_keypad = user_keypad.keypad +# assert (not all( +# post_shuffle_keypad[idx] == pre_shuffle_keypad[idx] for idx in range(len(post_shuffle_keypad)) +# )) +# assert (not all( +# post_shuffle_keypad[idx] != pre_shuffle_keypad[idx] for idx in range(len(post_shuffle_keypad)) +# ))