# 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 ```