diff --git a/docs/__init__.py b/docs/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/docs/nkode_authentication_template.md b/docs/nkode_authentication_template.md new file mode 100644 index 0000000..cbf4b32 --- /dev/null +++ b/docs/nkode_authentication_template.md @@ -0,0 +1,419 @@ +# nKode Authentication +Play around with the code in [this](http://sesolgit/Repository/Blob/92a60227-4ef9-4196-8ebb-595581abf98c?encodedName=main&encodedPath=nkode_tutorial.ipynb) jupyter notebook. + + +## Customer Creation +Before a user can be created, a customer with random attribute and set +values is created. The customers manage user's. They define an nKode policy, keypad's dimensions, +attributes/sets in the keypad, and the frequency of attribute renew. +### nKode Policy and Keypad Size +An nKode policy defines: + + +The keypad size defines: + + +The number of attributes must be greater than the number of keys to be [dispersion](nkode_concepts.md/#dispersion-resistant-interface) resistant. +``` +api = NKodeAPI() + +policy = NKodePolicy( + max_nkode_len=10, + min_nkode_len=4, + distinct_sets=0, + distinct_attributes=4, + byte_len=2 +) + +keypad_size = KeypadSize( + numb_of_keys = 5, + attrs_per_key = 6 # aka number of sets +) + +customer_id = api.create_new_customer(keypad_size, policy) +customer = api.customers[customer_id] +``` +### Customer Attributes and Sets +A customer has users and defines the attributes and set values for all its users. +Since our customer has 5 keys and 6 attributes per key, this gives a customer interface of 30 distinct attributes and 6 distinct attribute sets. +Each attribute belongs to one of the 6 sets. In this example, each attribute and set value is a unique 2 byte integer. + +``` +set_vals = customer.attributes.set_vals + +Customer Sets: {{ customer_set_vals }} +``` + +``` +attr_vals = customer.attributes.attr_vals +keypad_view(attr_vals, keypad_size.attrs_per_key) + +Customer Attributes: +{% for attrs in customer_attr_view -%} +{{ attrs }} +{% endfor %} +``` + +Attributes organized by set: +``` +attr_set_view = matrix_transpose(attr_keypad_view) +set_attribute_dict = dict(zip(set_vals, attr_set_view)) + +Set to Attribute Map: +{% for set_val, attrs in set_attribute_dict.items() -%} +{{ set_val }} : {{ attrs }} +{% endfor %} +``` + +## User Signup +Now that we have a customer, we can create users. To create a new user: + +1. Generate a random interface +2. User sets their nKode and sends their selection to the server +3. User confirms their nKode and the user is created if the nKode matches the nKode policy +### Random Interface Generation +For the server to determine the users nkode, the user's interface must be dispersable. +To make the interface dispersable, the server will randomly drop attribute sets to make the +number of attributes equal to the number of keys. In our case, the server randomly drops 1 attribute set +to give us a 5 X 5 keypad with possible index values ranging from 0-29. +Each value in the interface is the index value of a customer attribute. +The user never learns what their "real" attribute is. They don't see the index value that represents their nKode or +the customer value it is associated with. +``` +session_id, signup_interface = api.generate_index_interface(customer_id) +signup_interface_keypad = list_to_matrix(signup_interface, keypad_size.attrs_per_key) + +Signup Keypad: +{% for key in signup_keypad -%} +Key {{ loop.index }}: {{ key }} +{% endfor %} +``` + +### Set nKode +The user identifies attributes in the interface they want in their nkode. Each attribute has an index value. +Below the user has selected `{{ user_passcode }}`. These index values can be represented by anything in the GUI. +The only requirement is that the GUI attributes must be associated with the same index everytime the user goes to login. +If the user wants to change anything about their interface, they must also change their nkode. +``` +username = {{ username }} +user_passcode = {{ user_passcode }} +selected_keys_set = select_keys_with_passcode_values(user_passcode, signup_interface, keypad_size.attrs_per_key) + +Selected Keys +{{ selected_keys_set }} +``` + +The user's passcode server side attributes are: +``` +server_side_attr = [customer.attributes.attr_vals[idx] for idx in user_passcode] + +User Passcode Server-side Attributes: {{ server_side_attr }} +``` + +### Confirm nKode +The user submits the set interface to the sever and recieves the _confirm interface_ as a response. +The user finds their nKode again. +``` +confirm_interface = api.set_nkode(username, customer_id, selected_keys_set, session_id) +keypad_view(confirm_interface, keypad_size.numb_of_keys) +selected_keys_confirm = select_keys_with_passcode_values(user_passcode, confirm_interface, keypad_size.numb_of_keys) + +Confirm Keypad: +{% for key in confirm_keypad -%} +Key {{ loop.index }}: {{ key }} +{% endfor %} +Selected Keys: +{{ selected_keys_confirm }} +``` + +The user submits their confirm key selection and the user is created +``` +success = api.confirm_nkode(username, customer_id, selected_keys_confirm, session_id) +``` + +### Passcode Enciphering, Hashing, and Salting +When a new user creates an nKode, the server caches its set and confirm interface as well as the users key selection. +The on the last api.confirm_nkode the server: + +1. Deduces the users attributes +2. Validates the Passcode against the nKodePolicy +3. Creates new User Cipher Keys +4. Enciphers the user's mask +5. Enciphers, salts and hashes the user's passcode + +Steps 1-2 are straight forward. For a better idea of how they work, see pyNKode + +#### User Cipher Keys + +##### User Cipher Keys Data Structure +``` +set_key = generate_random_nonrepeating_list(keypad_size.attrs_per_key, max_numb=2**(8*numb_of_bytes)) +set_key = xor_lists(set_key, customer_attr.set_vals) + +UserCipherKeys( + alpha_key=generate_random_nonrepeating_list(keypad_size.attrs_per_key * keypad_size.numb_of_keys, max_numb=2**(8*numb_of_bytes)), + pass_key=generate_random_nonrepeating_list(max_nkode_len, max_numb=2**(8*numb_of_bytes)), + mask_key=generate_random_nonrepeating_list(max_nkode_len, max_numb=2**(8*numb_of_bytes)), + set_key=set_key, + salt=bcrypt.gensalt(), + max_nkode_len=max_nkode_len +) +``` + +##### User Cipher Keys Values +``` +user_keys = UserCipherKeys( + alpha_key = {{ user_keys.alpha_key }}, + pass_key = {{ user_keys.pass_key }}, + mask_key = {{ user_keys.mask_key }}, + set_key = {{ user_keys.set_key }}, + salt = {{ user_keys.salt }}, + max_nkode_len = {{ user_keys.max_nkode_len }} +) +``` + +The method UserCipherKeys.encipher_nkode secures a users nKode in the database. This method is called in api.confirm_nkode +``` +class EncipheredNKode(BaseModel): + code: str + mask: str +``` + +#### Mask Enciphering + +Recall: + +- set_key_i = (set_rand_numb_i ^ set_val_i) +- mask_key_i = mask_rand_numb_i +- padded_passcode_server_set_i = set_val_i +- len(set_key) == len(mask_key) == (padded_passcode_server_set) == max_nkode_len == 10 + where i is the index + +- mask_i = mask_key_i ^ padded_passcode_server_set_i ^ set_key_i +- mask_i = mask_rand_num_i ^ set_val_i ^ set_rand_numb_i ^ set_val_i +- mask_i = mask_rand_num_i ^ set_rand_numb_i # set_val_i is cancelled out + + +``` +# the passcode is deduced in confirm_nkode. These values are the index values of the customer attribute values +passcode = {{ user_passcode }} +passcode_server_attr = [customer.attributes.attr_vals[idx] for idx in passcode] +passcode_server_set = [customer.attributes.get_attr_set_val(attr) for attr in passcode_server_attr] + +Passcode Set Vals: {{ passcode_server_attr }} +Passcode Attr Vals: {{ passcode_server_set }} +``` + +``` +# pad passcode set list with random set values so the list is equal to the max nkode value. This hids the nKode's length +padded_passcode_server_set = user_keys.pad_user_mask(passcode_server_set, customer.nkode_policy.max_nkode_len) + +# get the index of each set value +set_idx = [customer.attributes.get_set_index(set_val) for set_val in padded_passcode_server_set] + +# find the set values matching set key to cancel out the set value +mask_set_keys = [user_keys.set_key[idx] for idx in set_idx] + +# xor the set key, passocode set value and the mask key +ciphered_mask = xor_lists(mask_set_keys, padded_passcode_server_set) +ciphered_mask = xor_lists(ciphered_mask, user_keys.mask_key) + +# encode ciphered mask in base64 +mask = user_keys.encode_base64_str(ciphered_mask) +Mask: {{ enciphered_nkode.mask }} +``` + +#### Passcode Enciphering and Hashing + +- ciphered_customer_attr = alpha_key ^ customer_attr +- ciphered_passcode_i = pass_key_i ^ ciphered_customer_attr_i +- code = hash(ciphered_passcode, salt) + +``` +ciphered_customer_attrs = xor_lists(customer.attributes.attr_vals, user_keys.alpha_key) +passcode_ciphered_attrs = [ciphered_customer_attrs[idx] for idx in 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) +passcode_digest = base64.b64encode(hashlib.sha256(passcode_bytes).digest()) +hashed_data = bcrypt.hashpw(passcode_digest, user_keys.salt) +code = hashed_data.decode("utf-8") + +Code: {{ enciphered_nkode.code }} +``` + +## User Login +1. Get login interface +2. Login + +### Get Login Interface +The client requests the user's login interface. +``` +login_interface = api.get_login_interface(username, customer_id) +keypad_view(login_interface, keypad_size.attrs_per_key) +``` +The server returns a randomly shuffled interface. Learn more about how the [User Interface Shuffle](nkode_concepts.md/#user-interface-shuffle) works +``` +Login Interface Keypad View: +{% for key in login_keypad -%} +Key {{ loop.index }}: {{ key }} +{% endfor %} +``` +Recall the user's passcode is `user_passcode = {{ user_passcode }}` so the user selects keys ` selected_keys_login = {{ selected_login_keys }}` + +``` +success = api.login(customer_id, username, selected_keys_login) +``` + +### Validate Login Key Entry +- decipher user mask and recover nkode set values +- get presumed attribute from key selection and set values +- encipher, salt and hash presumed attribute values and compare it to the users hashed code + +#### Decipher Mask +Recall: +- set_key_i = (set_key_rand_numb_i ^ set_val_i) +- mask_i = mask_key_rand_num_i ^ set_key_rand_numb_i + +Recover nKode set values: +- decode mask from base64 to int +- deciphered_mask = mask ^ mask_key +- deciphered_mask_i = set_key_rand_numb # mask_key_rand_num_i is cancelled out +- set_key_rand_component = set_key ^ set_values +- deduce the set value + +``` +user = customer.users[username] +user_keys = user.user_keys +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) +passcode_sets = [] +for set_cipher in deciphered_mask[:passcode_len]: + set_idx = set_key_rand_component.index(set_cipher) + passcode_sets.append(set_vals[set_idx]) + +Passcode Sets: {{ login_passcode_sets }} +``` + + +### Get Presumed Attributes +``` +set_vals_idx = [customer.attributes.get_set_index(set_val) for set_val in passcode_sets] + +presumed_selected_attributes_idx = [] +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) + presumed_selected_attributes_idx.append(selected_attr_idx) + +Presumped Passcode: {{ presumed_selected_attributes_idx }} +Recall User Passcode: {{ user_passcode }} +``` +### Compare Enciphered Passcodes +``` +enciphered_nkode = user_keys.encipher_salt_hash_code(presumed_selected_attributes_idx, customer.attributes) + +``` +If `enciphered_nkode == user.enciphered_passcode.code`, the user's key selection is valid and the login is successful. + +## Renew Attributes +Attributes renew is invoked with the renew_attributes method: `api.renew_attributes(customer_id)` +The renew attributes processes has three steps: +1. Renew Customer Attributes +2. Renew User Keys +3. Refresh User on Login + +When the `renew_attributes` method is called, the customer attributes are renewed and all it's users go through an intermediate +renew step. The user if fully renewed after their first successful login. This first login refreshes their keys, salt, and hash. + + +### Customer Renew +Old Customer attributes and set values are cached copied to variables before they are renewed. +``` +old_sets = customer.attributes.set_vals + +Customer Sets: {{ customer_set_vals }} +``` + +``` +old_attr = customer.attributes.attr_vals + +Customer Attributes: +{% for attrs in customer_attr_view -%} +{{ attrs }} +{% endfor %} +``` + +After the renew, the customer attributes and sets are new randomly generated values. +``` +api.renew_attributes(customer_id) + +set_vals = customer.attributes.set_vals + +Customer Sets: {{ customer_new_set_vals }} +``` + +``` +attr_vals = customer.attributes.attr_vals + +Customer Attributes: +{% for attrs in customer_new_attr_view -%} +{{ attrs }} +{% endfor %} +``` + +### Renew User +During the renew, each user goes through a temporary transition period. +``` +attrs_xor = xor_lists(new_attrs, old_attrs) +sets_xor = xor_lists(new_sets, old_sets) +for user in customer.users.values(): + user.renew = True + user.user_keys.set_key = xor_lists(user.user_keys.set_key, sets_xor) + user.user_keys.alpha_key = xor_lists(user.user_keys.alpha_key, attrs_xor) +``` +##### User Alpha Key +The user's alpha key was a randomly generated list of length `numb_of_keys * attr_per_key`. +Now each value in the alpha key is `alpha_key_i = old_alpha_key_i ^ new_attr_i ^ old_attr_i`. +Recall in the login process, `ciphered_customer_attrs = alpha_key ^ customer_attr`. +Since the customer_attr is now the new value, it gets cancelled out leaving: +``` +new_alpha_key = old_alpha_key ^ old_attr ^ new_attr +ciphered_customer_attrs = new_alpha_key ^ new_attr +ciphered_customer_attrs = old_alpha_key ^ old_attr # since new_attr cancel out +``` +We can valid the user's login attempt with the same hash using the new customer attributes + +##### User Set Key +The user's set key was a randomly generated list of length `attr_per_key` xor `customer_set_vals`. +Now the `old_set_vals` have been replaced with the new `new_set_vals`. The deciphering process described above +remains the same. + +### User Refresh +Once the user has a successful login, they get a new salt and cipher keys, and their `enciphered_passcode` is recomputed +with the new values. +``` +user.user_keys = UserCipherKeys.new( + customer.attributes.keypad_size, + customer.attributes.set_vals, + user.user_keys.max_nkode_len +) +user.enciphered_passcode = user.user_keys.encipher_nkode(presumed_selected_attributes_idx, customer.attributes) +user.renew = False +``` diff --git a/nkode_api.py b/nkode_api.py index f9e7c29..6f6f40c 100644 --- a/nkode_api.py +++ b/nkode_api.py @@ -95,6 +95,6 @@ class NKodeAPI(BaseModel): customer = self.customers[customer_id] return customer.valid_key_entry(username, key_selection) - def renew_keys(self, customer_id: UUID) -> bool: + def renew_attributes(self, customer_id: UUID) -> bool: assert (customer_id in self.customers.keys()) return self.customers[customer_id].renew_keys() diff --git a/nkode_tutorial.ipynb b/nkode_tutorial.ipynb index 699f066..b083088 100644 --- a/nkode_tutorial.ipynb +++ b/nkode_tutorial.ipynb @@ -12,12 +12,12 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-08-01T19:17:14.809230Z", - "start_time": "2024-08-01T19:17:14.807153Z" + "end_time": "2024-08-05T17:40:17.475149Z", + "start_time": "2024-08-05T17:40:17.473025Z" } }, "outputs": [], - "execution_count": 90 + "execution_count": 12 }, { "cell_type": "code", @@ -38,12 +38,12 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-08-01T19:17:14.881089Z", - "start_time": "2024-08-01T19:17:14.877818Z" + "end_time": "2024-08-05T17:40:17.556128Z", + "start_time": "2024-08-05T17:40:17.552985Z" } }, "outputs": [], - "execution_count": 91 + "execution_count": 13 }, { "cell_type": "code", @@ -53,12 +53,12 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-08-01T19:17:14.887878Z", - "start_time": "2024-08-01T19:17:14.885951Z" + "end_time": "2024-08-05T17:40:17.559851Z", + "start_time": "2024-08-05T17:40:17.557794Z" } }, "outputs": [], - "execution_count": 92 + "execution_count": 14 }, { "cell_type": "markdown", @@ -98,12 +98,12 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-08-01T19:17:15.071180Z", - "start_time": "2024-08-01T19:17:14.890419Z" + "end_time": "2024-08-05T17:40:17.740656Z", + "start_time": "2024-08-05T17:40:17.570041Z" } }, "outputs": [], - "execution_count": 93 + "execution_count": 15 }, { "cell_type": "markdown", @@ -132,13 +132,16 @@ "set_vals = customer.attributes.set_vals\n", "attr_vals = customer.attributes.attr_vals\n", "print(f\"Customer Sets: {set_vals}\")\n", - "keypad_view(attr_vals, keypad_size.attrs_per_key)" + "print(f\"Customer Attributes:\")\n", + "interface_keypad = list_to_matrix(attr_vals, keypad_size.attrs_per_key)\n", + "for idx, key_vals in enumerate(interface_keypad):\n", + " print(f\"{key_vals}\")" ], "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-08-01T19:17:15.075131Z", - "start_time": "2024-08-01T19:17:15.072535Z" + "end_time": "2024-08-05T17:40:17.744240Z", + "start_time": "2024-08-05T17:40:17.741773Z" } }, "outputs": [ @@ -146,17 +149,17 @@ "name": "stdout", "output_type": "stream", "text": [ - "Customer Sets: [27169, 36659, 54746, 56159, 47007, 38654]\n", - "Keypad View\n", - "Key 0: [63421, 44364, 4387, 12321, 14840, 38251]\n", - "Key 1: [42757, 19108, 4797, 54866, 10010, 33108]\n", - "Key 2: [61136, 42782, 46073, 53243, 53024, 53858]\n", - "Key 3: [44676, 54825, 25002, 7458, 37684, 16872]\n", - "Key 4: [65393, 10661, 14854, 8995, 64876, 36070]\n" + "Customer Sets: [41463, 23817, 10115, 59621, 17701, 39232]\n", + "Customer Attributes:\n", + "[15101, 42072, 1835, 15413, 46991, 41978]\n", + "[47133, 25183, 43097, 17581, 20893, 52753]\n", + "[14863, 31428, 41789, 18633, 3962, 50372]\n", + "[53357, 51786, 31149, 59745, 30907, 16058]\n", + "[60389, 36053, 8246, 53494, 23730, 15317]\n" ] } ], - "execution_count": 94 + "execution_count": 16 }, { "cell_type": "markdown", @@ -180,8 +183,8 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-08-01T19:17:15.078753Z", - "start_time": "2024-08-01T19:17:15.076083Z" + "end_time": "2024-08-05T17:40:17.747131Z", + "start_time": "2024-08-05T17:40:17.744937Z" } }, "outputs": [ @@ -190,16 +193,16 @@ "output_type": "stream", "text": [ "Set to Attribute Map:\n", - "27169: [63421, 42757, 61136, 44676, 65393]\n", - "36659: [44364, 19108, 42782, 54825, 10661]\n", - "54746: [4387, 4797, 46073, 25002, 14854]\n", - "56159: [12321, 54866, 53243, 7458, 8995]\n", - "47007: [14840, 10010, 53024, 37684, 64876]\n", - "38654: [38251, 33108, 53858, 16872, 36070]\n" + "41463: [15101, 47133, 14863, 53357, 60389]\n", + "23817: [42072, 25183, 31428, 51786, 36053]\n", + "10115: [1835, 43097, 41789, 31149, 8246]\n", + "59621: [15413, 17581, 18633, 59745, 53494]\n", + "17701: [46991, 20893, 3962, 30907, 23730]\n", + "39232: [41978, 52753, 50372, 16058, 15317]\n" ] } ], - "execution_count": 95 + "execution_count": 17 }, { "cell_type": "markdown", @@ -229,27 +232,27 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-08-01T19:17:15.083395Z", - "start_time": "2024-08-01T19:17:15.080158Z" + "end_time": "2024-08-05T17:40:17.751757Z", + "start_time": "2024-08-05T17:40:17.748531Z" } }, "outputs": [ { "data": { "text/plain": [ - "[[25, 14, 15, 23, 28],\n", - " [13, 26, 21, 17, 4],\n", - " [19, 20, 27, 29, 10],\n", - " [1, 8, 3, 5, 22],\n", - " [7, 2, 9, 11, 16]]" + "[[29, 3, 19, 12, 26],\n", + " [5, 27, 1, 0, 8],\n", + " [23, 9, 13, 24, 20],\n", + " [11, 21, 25, 18, 14],\n", + " [17, 15, 7, 6, 2]]" ] }, - "execution_count": 96, + "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], - "execution_count": 96 + "execution_count": 18 }, { "cell_type": "markdown", @@ -270,13 +273,15 @@ "user_passcode = signup_interface[:passcode_len]\n", "selected_keys_set = select_keys_with_passcode_values(user_passcode, signup_interface, keypad_size.numb_of_keys)\n", "print(f\"User Passcode: {user_passcode}\")\n", - "print(f\"Selected Keys\\n{selected_keys_set}\")" + "print(f\"Selected Keys\\n{selected_keys_set}\")\n", + "server_side_attr = [customer.attributes.attr_vals[idx] for idx in user_passcode]\n", + "print(f\"User Passcode Server-side Attributes: {server_side_attr}\")" ], "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-08-01T19:17:15.087201Z", - "start_time": "2024-08-01T19:17:15.084377Z" + "end_time": "2024-08-05T17:40:17.755453Z", + "start_time": "2024-08-05T17:40:17.752507Z" } }, "outputs": [ @@ -285,18 +290,24 @@ "output_type": "stream", "text": [ "Keypad View\n", - "Key 0: [25, 14, 15, 23, 28]\n", - "Key 1: [13, 26, 21, 17, 4]\n", - "Key 2: [19, 20, 27, 29, 10]\n", - "Key 3: [1, 8, 3, 5, 22]\n", - "Key 4: [7, 2, 9, 11, 16]\n", - "User Passcode: [25, 14, 15, 23]\n", + "Key 0: [29, 3, 19, 12, 26]\n", + "Key 1: [5, 27, 1, 0, 8]\n", + "Key 2: [23, 9, 13, 24, 20]\n", + "Key 3: [11, 21, 25, 18, 14]\n", + "Key 4: [17, 15, 7, 6, 2]\n", + "User Passcode: [29, 3, 19, 12]\n", "Selected Keys\n", - "[0, 0, 0, 0]\n" + "[0, 0, 0, 0]\n", + "User Passcode Server-side Attributes: [15317, 15413, 51786, 14863]\n" ] } ], - "execution_count": 97 + "execution_count": 19 + }, + { + "metadata": {}, + "cell_type": "markdown", + "source": "" }, { "cell_type": "code", @@ -309,8 +320,8 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-08-01T19:17:15.090959Z", - "start_time": "2024-08-01T19:17:15.088160Z" + "end_time": "2024-08-05T17:40:17.760514Z", + "start_time": "2024-08-05T17:40:17.758171Z" } }, "outputs": [ @@ -319,17 +330,17 @@ "output_type": "stream", "text": [ "Keypad View\n", - "Key 0: [1, 14, 21, 11, 10]\n", - "Key 1: [19, 2, 15, 5, 4]\n", - "Key 2: [13, 8, 9, 29, 28]\n", - "Key 3: [25, 20, 3, 17, 16]\n", - "Key 4: [7, 26, 27, 23, 22]\n", + "Key 0: [23, 3, 25, 6, 8]\n", + "Key 1: [17, 9, 19, 0, 14]\n", + "Key 2: [5, 15, 13, 18, 26]\n", + "Key 3: [11, 27, 7, 12, 20]\n", + "Key 4: [29, 21, 1, 24, 2]\n", "Selected Keys\n", - "[3, 0, 1, 4]\n" + "[4, 0, 1, 3]\n" ] } ], - "execution_count": 98 + "execution_count": 20 }, { "cell_type": "code", @@ -341,8 +352,8 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-08-01T19:17:15.725645Z", - "start_time": "2024-08-01T19:17:15.091674Z" + "end_time": "2024-08-05T17:40:18.401946Z", + "start_time": "2024-08-05T17:40:17.761154Z" } }, "outputs": [ @@ -354,7 +365,7 @@ ] } ], - "execution_count": 99 + "execution_count": 21 }, { "metadata": {}, @@ -373,26 +384,50 @@ { "metadata": { "ExecuteTime": { - "end_time": "2024-08-01T19:17:16.066633Z", - "start_time": "2024-08-01T19:17:15.726507Z" + "end_time": "2024-08-05T17:54:37.484873Z", + "start_time": "2024-08-05T17:54:37.480227Z" } }, "cell_type": "code", "source": [ "from src.user_cipher_keys import UserCipherKeys\n", + "from src.utils import xor_lists\n", "\n", - "user_keys = UserCipherKeys.new(\n", - " customer.attributes.keypad_size,\n", - " customer.attributes.set_vals,\n", - " customer.nkode_policy.max_nkode_len\n", + "\n", + "set_key = [46785, 4782, 4405, 44408, 35377, 55527]\n", + "set_key = xor_lists(set_key, customer.attributes.set_vals)\n", + "user_keys = UserCipherKeys(\n", + " alpha_key = [\n", + " 57200, 8398, 54694, 25997, 30388,\n", + " 46948, 45549, 30364, 49712, 10447,\n", + " 9205, 1777, 10731, 30979, 2795,\n", + " 17068, 56758, 62574, 28641, 11451,\n", + " 26820, 50373, 48783, 25350, 62177,\n", + " 60608, 54242, 4637, 3525, 16313\n", + " ],\n", + " pass_key=[16090, 38488, 45111, 32674, 46216, 52013, 48980, 36811, 35296, 17206],\n", + " mask_key=[29575, 43518, 44373, 62063, 37651, 31671, 31663, 65514, 36454, 47325],\n", + " set_key=set_key,\n", + " salt=b'$2b$12$fX.in.GGAjz3QBBwqSWc6e',\n", + " max_nkode_len=customer.nkode_policy.max_nkode_len, \n", ")\n", "\n", - "passcode = [10, 23, 14, 7]\n", - "passcode_server_attr = [customer.attributes.attr_vals[idx] for idx in passcode]\n", - "passcode_server_set = [customer.attributes.get_attr_set_val(attr) for attr in passcode_server_attr]" + "passcode_server_attr = [customer.attributes.attr_vals[idx] for idx in user_passcode]\n", + "passcode_server_set = [customer.attributes.get_attr_set_val(attr) for attr in passcode_server_attr]\n", + "print(f\"Passcode Set Vals: {passcode_server_set}\")\n", + "print(f\"Passcode Attr Vals: {passcode_server_attr}\")" ], - "outputs": [], - "execution_count": 100 + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Passcode Set Vals: [17701, 39232, 10115, 23817]\n", + "Passcode Attr Vals: [20893, 16058, 41789, 25183]\n" + ] + } + ], + "execution_count": 36 }, { "metadata": {}, @@ -414,8 +449,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2024-08-01T19:17:16.070242Z", - "start_time": "2024-08-01T19:17:16.067409Z" + "end_time": "2024-08-05T17:52:29.553010Z", + "start_time": "2024-08-05T17:52:29.549861Z" } }, "cell_type": "code", @@ -431,7 +466,7 @@ "mask = user_keys.encode_base64_str(ciphered_mask)" ], "outputs": [], - "execution_count": 101 + "execution_count": 28 }, { "metadata": {}, @@ -448,8 +483,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2024-08-01T19:17:16.378573Z", - "start_time": "2024-08-01T19:17:16.072571Z" + "end_time": "2024-08-05T17:52:34.104235Z", + "start_time": "2024-08-05T17:52:33.794884Z" } }, "cell_type": "code", @@ -473,13 +508,13 @@ "code = hashed_data.decode(\"utf-8\")" ], "outputs": [], - "execution_count": 102 + "execution_count": 29 }, { "metadata": { "ExecuteTime": { - "end_time": "2024-08-01T19:17:16.381295Z", - "start_time": "2024-08-01T19:17:16.379384Z" + "end_time": "2024-08-05T17:52:34.205433Z", + "start_time": "2024-08-05T17:52:34.203278Z" } }, "cell_type": "code", @@ -492,7 +527,7 @@ ")" ], "outputs": [], - "execution_count": 103 + "execution_count": 30 }, { "cell_type": "markdown", @@ -518,8 +553,8 @@ "metadata": { "collapsed": false, "ExecuteTime": { - "end_time": "2024-08-01T19:17:16.687966Z", - "start_time": "2024-08-01T19:17:16.382136Z" + "end_time": "2024-08-05T17:52:38.297363Z", + "start_time": "2024-08-05T17:52:37.990052Z" } }, "outputs": [ @@ -528,17 +563,17 @@ "output_type": "stream", "text": [ "Keypad View\n", - "Key 0: [0, 7, 20, 3, 10, 29]\n", - "Key 1: [6, 25, 26, 15, 4, 17]\n", - "Key 2: [12, 19, 14, 21, 28, 23]\n", - "Key 3: [18, 1, 2, 9, 16, 11]\n", - "Key 4: [24, 13, 8, 27, 22, 5]\n", - "Selected Keys: [1, 2, 1, 2]\n", + "Key 0: [6, 19, 14, 15, 4, 17]\n", + "Key 1: [24, 13, 26, 3, 28, 23]\n", + "Key 2: [0, 7, 2, 9, 16, 5]\n", + "Key 3: [18, 1, 8, 27, 22, 11]\n", + "Key 4: [12, 25, 20, 21, 10, 29]\n", + "Selected Keys: [4, 1, 0, 4]\n", "True\n" ] } ], - "execution_count": 104 + "execution_count": 31 }, { "metadata": {}, @@ -570,13 +605,14 @@ { "metadata": { "ExecuteTime": { - "end_time": "2024-08-01T19:17:16.691445Z", - "start_time": "2024-08-01T19:17:16.688722Z" + "end_time": "2024-08-05T17:59:58.753205Z", + "start_time": "2024-08-05T17:59:58.749714Z" } }, "cell_type": "code", "source": [ "user = customer.users[username]\n", + "set_vals = customer.attributes.set_vals\n", "user_keys = user.user_keys\n", "user_mask = user.enciphered_passcode.mask\n", "decoded_mask = user_keys.decode_base64_str(user_mask)\n", @@ -593,11 +629,11 @@ "name": "stdout", "output_type": "stream", "text": [ - "[36659, 54746, 56159, 38654]\n" + "[39232, 59621, 23817, 41463]\n" ] } ], - "execution_count": 105 + "execution_count": 38 }, { "metadata": {}, @@ -607,8 +643,8 @@ { "metadata": { "ExecuteTime": { - "end_time": "2024-08-01T19:17:16.694718Z", - "start_time": "2024-08-01T19:17:16.692225Z" + "end_time": "2024-08-05T17:54:03.252644Z", + "start_time": "2024-08-05T17:54:03.249271Z" } }, "cell_type": "code", @@ -633,7 +669,7 @@ ] } ], - "execution_count": 106 + "execution_count": 35 }, { "metadata": {}, @@ -641,34 +677,21 @@ "source": "### Compare Enciphered Passcodes" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2024-08-01T19:17:17.000271Z", - "start_time": "2024-08-01T19:17:16.695443Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "enciphered_nkode = user_keys.encipher_salt_hash_code(presumed_selected_attributes_idx, customer.attributes)\n", "\n", "print(enciphered_nkode == user.enciphered_passcode.code)\n" ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "True\n" - ] - } - ], - "execution_count": 107 + "outputs": [], + "execution_count": null }, { "cell_type": "markdown", "source": [ - "#### Renew Keys\n", - "1. Renew Customer Keys\n", + "## Renew Attributes \n", + "1. Renew Customer Attributes \n", "2. Renew User Keys\n", "3. Refresh User on Login\n", "\n" @@ -678,12 +701,7 @@ } }, { - "metadata": { - "ExecuteTime": { - "end_time": "2024-08-01T19:17:18.107245Z", - "start_time": "2024-08-01T19:17:17.001318Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "def print_user_enciphered_code():\n", @@ -692,7 +710,7 @@ " print(f\"mask: {mask}, code: {code}\")\n", "\n", "print_user_enciphered_code() \n", - "api.renew_keys(customer_id)\n", + "api.renew_attributes(customer_id)\n", "print_user_enciphered_code()\n", "\n", "login_interface = api.get_login_interface(username, customer_id)\n", @@ -701,19 +719,8 @@ "print(success)\n", "print_user_enciphered_code()" ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "mask: AH3tBMcj4vYN+WBQa4Wgwe3KDgE=, code: $2b$12$2w4nllgf0wnWMsdTYL4HNuxVN5A5AzqP5X2Xraj0ijKCAjM3kRlLm\n", - "mask: AH3tBMcj4vYN+WBQa4Wgwe3KDgE=, code: $2b$12$2w4nllgf0wnWMsdTYL4HNuxVN5A5AzqP5X2Xraj0ijKCAjM3kRlLm\n", - "True\n", - "mask: pbB0tDKAFPtJUEQjyUZBdSCRsIc=, code: $2b$12$KW3WJQ2YNsbmyWJ6Rq32eed4/GJ6howBNsKA/sUhFOHRf.sIW2Pz6\n" - ] - } - ], - "execution_count": 108 + "outputs": [], + "execution_count": null }, { "metadata": {}, @@ -725,12 +732,7 @@ ] }, { - "metadata": { - "ExecuteTime": { - "end_time": "2024-08-01T19:17:18.281874Z", - "start_time": "2024-08-01T19:17:18.108020Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "old_attrs = customer.attributes.attr_vals.copy()\n", @@ -740,7 +742,7 @@ "new_sets = customer.attributes.set_vals" ], "outputs": [], - "execution_count": 109 + "execution_count": null }, { "metadata": {}, @@ -751,12 +753,7 @@ ] }, { - "metadata": { - "ExecuteTime": { - "end_time": "2024-08-01T19:17:18.285195Z", - "start_time": "2024-08-01T19:17:18.282828Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "attrs_xor = xor_lists(new_attrs, old_attrs)\n", @@ -767,7 +764,7 @@ " user.user_keys.alpha_key = xor_lists(user.user_keys.alpha_key, attrs_xor)" ], "outputs": [], - "execution_count": 110 + "execution_count": null }, { "metadata": {}, @@ -775,12 +772,7 @@ "source": "### Refresh User Keys" }, { - "metadata": { - "ExecuteTime": { - "end_time": "2024-08-01T19:17:18.921478Z", - "start_time": "2024-08-01T19:17:18.286363Z" - } - }, + "metadata": {}, "cell_type": "code", "source": [ "user.user_keys = UserCipherKeys.new(\n", @@ -792,7 +784,7 @@ "user.renew = False" ], "outputs": [], - "execution_count": 111 + "execution_count": null } ], "metadata": { diff --git a/render_markdown.py b/render_markdown.py new file mode 100644 index 0000000..be6e843 --- /dev/null +++ b/render_markdown.py @@ -0,0 +1,216 @@ +from jinja2 import Environment, FileSystemLoader +import os +from nkode_api import NKodeAPI +from src.models import NKodePolicy, KeypadSize, EncipheredNKode +from src.user_cipher_keys import UserCipherKeys +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: + return "test_username" + "".join([choice(ascii_lowercase) for _ in range(6)]) + + +def select_keys_with_passcode_values(user_passcode: list[int], interface: list[int], attrs_per_key: int) -> list[int]: + return [interface.index(attr) // attrs_per_key for attr in user_passcode] + + +def keypad_view(interface: list[int], attrs_per_key: int): + print("Keypad View") + interface_keypad = list_to_matrix(interface, attrs_per_key) + for idx, key_vals in enumerate(interface_keypad): + print(f"Key {idx}: {key_vals}") + + +def render_nkode_authentication(data: dict): + # Set up the Jinja2 environment and template loader + file_loader = FileSystemLoader('./docs') + env = Environment(loader=file_loader) + + # Load the template + template = env.get_template('nkode_authentication_template.md') + + print(os.getcwd()) + # Render the template with the data + output = template.render(data) + + # Print or save the output + output_file = "/Users/donov/Desktop/NKode_documentation/nkode/docs/nkode_authentication.md" + with open(output_file, 'w') as fp: + fp.write(output) + print("File written successfully") + + +if __name__ == "__main__": + api = NKodeAPI() + + policy = NKodePolicy( + max_nkode_len=10, + min_nkode_len=4, + distinct_sets=0, + distinct_attributes=4, + byte_len=2, + ) + keypad_size = KeypadSize( + numb_of_keys=5, + attrs_per_key=6 # aka number of sets + ) + customer_id = api.create_new_customer(keypad_size, policy) + customer = api.customers[customer_id] + + set_vals = customer.attributes.set_vals + attr_vals = customer.attributes.attr_vals + customer_attr_view = list_to_matrix(attr_vals, keypad_size.attrs_per_key) + + attr_keypad_view = list_to_matrix(attr_vals, keypad_size.attrs_per_key) + attr_set_view = matrix_transpose(attr_keypad_view) + 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) + + username = random_username() + passcode_len = 4 + user_passcode = signup_interface[:passcode_len] + selected_keys_set = select_keys_with_passcode_values(user_passcode, signup_interface, keypad_size.numb_of_keys) + server_side_attr = [customer.attributes.attr_vals[idx] for idx in user_passcode] + + confirm_interface = api.set_nkode(username, customer_id, selected_keys_set, session_id) + + confirm_keypad = list_to_matrix(confirm_interface, keypad_size.numb_of_keys) + + selected_keys_confirm = select_keys_with_passcode_values(user_passcode, confirm_interface, keypad_size.numb_of_keys) + + success = api.confirm_nkode(username, customer_id, selected_keys_confirm, session_id) + assert success + passcode_server_attr = [customer.attributes.attr_vals[idx] for idx in user_passcode] + passcode_server_set = [customer.attributes.get_attr_set_val(attr) for attr in passcode_server_attr] + + user_keys = customer.users[username].user_keys + + padded_passcode_server_set = user_keys.pad_user_mask(passcode_server_set, customer.attributes.set_vals) + + set_idx = [customer.attributes.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) + mask = user_keys.encode_base64_str(ciphered_mask) + + ciphered_customer_attrs = xor_lists(customer.attributes.attr_vals, user_keys.alpha_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) + passcode_digest = base64.b64encode(hashlib.sha256(passcode_bytes).digest()) + hashed_data = bcrypt.hashpw(passcode_digest, user_keys.salt) + code = hashed_data.decode("utf-8") + + enciphered_nkode = EncipheredNKode( + mask=mask, + code=code, + ) + """ + USER LOGIN + """ + login_interface = api.get_login_interface(username, customer_id) + login_keypad = list_to_matrix(login_interface, keypad_size.attrs_per_key) + selected_keys_login = select_keys_with_passcode_values(user_passcode, login_interface, keypad_size.attrs_per_key) + success = api.login(customer_id, username, selected_keys_login) + assert success + + """ + VALIDATE LOGIN KEY ENTRY + DECIPHER MASK + """ + + user = customer.users[username] + set_vals = customer.attributes.set_vals + user_keys = user.user_keys + 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) + login_passcode_sets = [] + for set_cipher in deciphered_mask[:passcode_len]: + set_idx = set_key_rand_component.index(set_cipher) + login_passcode_sets.append(set_vals[set_idx]) + + """ + GET PRESUMED ATTRIBUTES + """ + + set_vals_idx = [customer.attributes.get_set_index(set_val) for set_val in login_passcode_sets] + + presumed_selected_attributes_idx = [] + 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) + presumed_selected_attributes_idx.append(selected_attr_idx) + + """ + RENEW KEYS + """ + + old_attrs = customer.attributes.attr_vals.copy() + old_sets = customer.attributes.set_vals.copy() + customer.attributes.renew() + new_attrs = customer.attributes.attr_vals + new_sets = customer.attributes.set_vals + customer_new_attr_view = list_to_matrix(new_attrs, keypad_size.attrs_per_key) + + """ + RENEW USER + """ + attrs_xor = xor_lists(new_attrs, old_attrs) + sets_xor = xor_lists(new_sets, old_sets) + for user in customer.users.values(): + user.renew = True + user.user_keys.set_key = xor_lists(user.user_keys.set_key, sets_xor) + user.user_keys.alpha_key = xor_lists(user.user_keys.alpha_key, attrs_xor) + + """ + REFRESH USER KEYS + """ + user.user_keys = UserCipherKeys.new( + customer.attributes.keypad_size, + customer.attributes.set_vals, + user.user_keys.max_nkode_len + ) + user.enciphered_passcode = user.user_keys.encipher_nkode(presumed_selected_attributes_idx, customer.attributes) + user.renew = False + + # Define some data to pass to the template + data = { + 'customer_set_vals': set_vals, + 'customer_attr_view': customer_attr_view, + 'set_attribute_dict': set_attribute_dict, + 'signup_keypad': signup_keypad, + 'username': 'test_user', + 'user_passcode': user_passcode, + 'selected_keys_set': selected_keys_set, + 'server_side_attr': server_side_attr, + 'confirm_keypad': confirm_keypad, + 'selected_keys_confirm': selected_keys_confirm, + 'user_keys': user_keys, + 'passcode_server_attr': passcode_server_attr, + 'passcode_server_set': passcode_server_set, + 'enciphered_nkode': enciphered_nkode, + 'login_keypad': login_keypad, + 'selected_login_keys': selected_keys_login, + 'login_passcode_sets': login_passcode_sets, + 'presumed_selected_attributes_idx': presumed_selected_attributes_idx, + 'customer_new_attr_view': customer_new_attr_view, + 'customer_new_set_vals': new_sets, + + } + render_nkode_authentication(data) \ No newline at end of file diff --git a/test/test_nkode_api.py b/test/test_nkode_api.py index 87cf18d..7720beb 100644 --- a/test/test_nkode_api.py +++ b/test/test_nkode_api.py @@ -38,7 +38,7 @@ def test_create_new_user_and_renew_keys(nkode_api, keypad_size, passocode_len): successful_login = nkode_api.login(customer_id, username, login_key_selection) assert successful_login - successful_renew = nkode_api.renew_keys(customer_id) + successful_renew = nkode_api.renew_attributes(customer_id) assert successful_renew login_interface = nkode_api.get_login_interface(username, customer_id)