From 41a7e14fb4cad573e979c6c4e0893a94394f995d Mon Sep 17 00:00:00 2001 From: Donovan Date: Fri, 14 Mar 2025 10:22:22 -0500 Subject: [PATCH] refactor nkode_authentication_template.md --- README.md | 446 ++++++++++++++++++ ...ication_template.md => readme_template.md} | 4 +- docs/{render_markdown.py => render_readme.py} | 5 +- test/test_nkode_api.py | 6 +- 4 files changed, 454 insertions(+), 7 deletions(-) create mode 100644 README.md rename docs/{nkode_authentication_template.md => readme_template.md} (98%) rename docs/{render_markdown.py => render_readme.py} (98%) diff --git a/README.md b/README.md new file mode 100644 index 0000000..ff4768f --- /dev/null +++ b/README.md @@ -0,0 +1,446 @@ +# README +Play around with the code in /notebooks + +## Customer Creation +Before creating a user, a customer generates random properties and set +values. The customers manage users. They define an nKode policy, keypad's dimensions, +properties/sets in the keypad, and the frequency of property renewal. +### nKode Policy and Keypad Size +An nKode policy defines: + + +The keypad size defines: + + +To be [dispersion](nkode_concepts.md/#dispersion-resistant-keypad) resistant, the number of properties must be greater than the number of keys. + +``` +api = NKodeAPI() + +policy = NKodePolicy( + max_nkode_len=10, + min_nkode_len=4, + distinct_sets=0, + distinct_properties=4, + byte_len=2 +) + +keypad_size = KeypadSize( + numb_of_keys = 5, + props_per_key = 6 # aka number of sets +) + +customer_id = api.create_new_customer(keypad_size, policy) +customer = api.customers[customer_id] +``` +### Customer properties and Sets +A customer has users and defines the properties and set values for all its users. +Since our customer has 5 keys and 6 properties per key, +this gives a customer keypad of 30 distinct properties and 6 distinct property sets. +Each property belongs to one of the 6 sets. Each property and set value is a unique 2-byte integer in this example. + +``` +set_vals = customer.cipher.set_key + +Customer Sets: [51397 49224 50087 24444 43554 21522] +``` + +``` +prop_vals = customer.cipher.prop_key +keypad_view(prop_vals, keypad_size.props_per_key) + +Customer properties: +[65030 40058 49729 42519 32475 21731] +[19446 3351 17075 17586 20753 15754] +[19712 56685 43602 30750 54931 27419] +[40397 10398 13477 26037 17943 47642] +[58359 15284 53370 4343 16407 46898] + +``` + +properties organized by set: +``` +prop_set_view = matrix_transpose(prop_keypad_view) +set_property_dict = dict(zip(set_vals, prop_set_view)) + +Set to property Map: +51397 : [65030 19446 19712 40397 58359] +49224 : [40058 3351 56685 10398 15284] +50087 : [49729 17075 43602 13477 53370] +24444 : [42519 17586 30750 26037 4343] +43554 : [32475 20753 54931 17943 16407] +21522 : [21731 15754 27419 47642 46898] + +``` + +## User Signup +Now that we have a customer, we can create users. To create a new user: + +1. Generate a random keypad +2. The user sets their nKode and sends their selection to the server +3. The user confirms their nKode. If the user's nKode matches the policy, the server creates the user. +### Random keypad Generation +The user's keypad must be dispersable so the server can determine the user's nkode. +The server randomly drops property sets until +the number of properties equals the number of keys, making the keypad dispersable. +In our case, the server randomly drops 1 property set. +to give us a 5 X 5 keypad with possible index values ranging from 0-29. +Each value in the keypad is the index value of a customer property. +The user never learns what their "real" property is. They do not see the index value representing their nKode or +the customer server-side value. + +``` +session_id, signup_keypad = api.generate_index_keypad(customer_id) +signup_keypad_keypad = list_to_matrix(signup_keypad, keypad_size.props_per_key) + +Signup Keypad: +Key 1: [19 7 25 1 13] +Key 2: [18 6 24 0 12] +Key 3: [21 9 27 3 15] +Key 4: [23 11 29 5 17] +Key 5: [20 8 26 2 14] + +``` + +### Set nKode +The user identifies properties in the keypad they want in their nkode. Each property has an index value. +Below, the user has selected `[19, 7, 25, 1]`. These index values can be represented by anything in the GUI. +The only requirement is that the GUI properties be associated with the same index every time the user logs in. +If users want to change anything about their keypad, they must also change their nkode. + +``` +username = test_user +user_passcode = [19, 7, 25, 1] +selected_keys_set = select_keys_with_passcode_values(user_passcode, signup_keypad, keypad_size.props_per_key) + +Selected Keys +[0, 0, 0, 0] +``` + +The user's passcode server side properties are: +``` +server_side_prop = [customer.cipher.prop_key[idx] for idx in user_passcode] + +User Passcode Server-side properties: [np.int64(10398), np.int64(3351), np.int64(15284), np.int64(40058)] +``` + +### Confirm nKode +The user submits the set keypad to the server and receives the _confirm keypad_ as a response. +The user finds their nKode again. + +``` +confirm_keypad = api.set_nkode(username, customer_id, selected_keys_set, session_id) +keypad_view(confirm_keypad, keypad_size.numb_of_keys) +selected_keys_confirm = select_keys_with_passcode_values(user_passcode, confirm_keypad, keypad_size.numb_of_keys) + +Confirm Keypad: +Key 1: [20 7 27 5 12] +Key 2: [23 9 26 0 13] +Key 3: [18 8 29 1 15] +Key 4: [19 11 24 3 14] +Key 5: [21 6 25 2 17] + +Selected Keys: +[3, 0, 4, 2] +``` + +The user submits their confirmation 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 confirms the keypad and the user's key selection. +On the last api.confirm_nkode, the server: + +1. Deduces the user's properties +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 straightforward. 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.props_per_key, max_numb=2**(8*numb_of_bytes)) +set_key = xor_lists(set_key, customer_prop.set_vals) + +UserCipherKeys( + prop_key=generate_random_nonrepeating_list(keypad_size.props_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_cipher = UserCipherKeys( + prop_key = [ 2923 16019 14458 50197 31207 7212 56686 44981 2641 64112 13044 29822 + 1902 22608 40919 35763 49353 20507 18363 34108 32269 6440 21357 37870 + 60382 18170 45147 13683 20896 12198], + pass_key = [31251 55189 60990 1342 51754 25296 19081 956 41188 43289], + mask_key = [54532 41537 22695 64404 28419 7322 24742 54924 2951 57084], + set_key = [ 3824 27422 49987 58720 10692 60061], + salt = b'$2b$12$iLYVBzbu9DVSg7S.ZBzB..', + max_nkode_len = 10 +) +``` + +The method UserCipherKeys.encipher_nkode secures a user's 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 + + +``` +passcode = [19, 7, 25, 1] +passcode_server_prop = [customer.cipher.prop_key[idx] for idx in passcode] +passcode_server_set = [customer.cipher.get_prop_set_val(prop) for prop in passcode_server_prop] + +Passcode Set Vals: [np.int64(10398), np.int64(3351), np.int64(15284), np.int64(40058)] +Passcode prop Vals: [49224, 49224, 49224, 49224] +``` + +``` +padded_passcode_server_set = user_cipher.pad_user_mask(passcode_server_set, customer.nkode_policy.max_nkode_len) + +set_idx = [customer.cipher.get_set_index(set_val) for set_val in padded_passcode_server_set] +mask_set_keys = [user_cipher.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_cipher.mask_key) + +mask = user_cipher.encode_base64_str(ciphered_mask) +Mask: c6kE7P4KXTm3d3KmDprj8dPzBog= +``` + +#### Passcode Enciphering and Hashing + +- ciphered_customer_prop = prop_key ^ customer_prop +- ciphered_passcode_i = pass_key_i ^ ciphered_customer_prop_i +- code = hash(ciphered_passcode, salt) + +``` +ciphered_customer_props = xor_lists(customer.cipher.prop_key, user_cipher.prop_key) +passcode_ciphered_props = [ciphered_customer_props[idx] for idx in passcode] +pad_len = customer.nkode_policy.max_nkode_len - passcode_len + +passcode_ciphered_props.extend([0 for _ in range(pad_len)]) + +ciphered_code = xor_lists(passcode_ciphered_props, user_cipher.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_cipher.salt) +code = hashed_data.decode("utf-8") + +Code: $2b$12$iLYVBzbu9DVSg7S.ZBzB..eoFhCtiWBtfjXNLULtODYBH8Epva1pC +``` + +## User Login +To login, a user: + +1. Gets login keypad +2. Submits key entry + +### Get Login keypad +The client requests the user's login keypad. +``` +login_keypad = api.get_login_keypad(username, customer_id) +keypad_view(login_keypad, keypad_size.props_per_key) +``` +The server returns a randomly shuffled keypad. Learn more about how the [User keypad Shuffle](nkode_concepts.md/#user-keypad-shuffle) works +``` +Login keypad Keypad View: +Key 1: [18 19 20 21 22 23] +Key 2: [ 6 7 8 9 10 11] +Key 3: [24 25 26 27 28 29] +Key 4: [0 1 2 3 4 5] +Key 5: [12 13 14 15 16 17] + +``` +Recall the user's passcode is `user_passcode = [19, 7, 25, 1]` so the user selects keys ` selected_keys_login = [0, 1, 2, 3]` + +``` +success = api.login(customer_id, username, selected_keys_login) +``` + +### Validate Login Key Entry +- decipher user mask and recover nkode set values +- get presumed property from key selection and set values +- encipher, salt, and hash presumed property values and compare them 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_cipher = user.user_cipher +user_mask = user.enciphered_passcode.mask +decoded_mask = user_cipher.decode_base64_str(user_mask) +deciphered_mask = xor_lists(decoded_mask, user_cipher.mask_key) +set_key_rand_component = xor_lists(set_vals, user_cipher.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: [49224, 49224, 49224, 49224] +``` + + +### Get Presumed properties +``` +set_vals_idx = [customer.cipher.get_set_index(set_val) for set_val in passcode_sets] + +presumed_selected_properties_idx = [] +for idx in range(passcode_len): + key_numb = selected_keys_login[idx] + set_idx = set_vals_idx[idx] + selected_prop_idx = customer.users[username].user_keypad.get_prop_idx_by_keynumb_setidx(key_numb, set_idx) + presumed_selected_properties_idx.append(selected_prop_idx) + +Presumped Passcode: [19, 7, 25, 1] +Recall User Passcode: [19, 7, 25, 1] +``` +### Compare Enciphered Passcodes +``` +enciphered_nkode = user_cipher.encipher_salt_hash_code(presumed_selected_properties_idx, customer.cipher) +``` +If `enciphered_nkode == user.enciphered_passcode.code`, the user's key selection is valid, and the login is successful. + +## Renew properties +properties renew is invoked with the renew_properties method: `api.renew_properties(customer_id)` +The renew properties process has three steps: +1. Renew Customer properties +2. Renew User Keys +3. Refresh User on Login + +When the customer calls the `renew_properties` method, the method replaces the customer's properties and set values. All its users go through an intermediate +renewal step. The users fully renew after their first successful login. This first login refreshes their keys, salt, and hash with new values. + + +### Customer Renew +Old Customer properties and set values are cached and copied to variables before renewal. +``` +old_sets = customer.cipher.set_key + +Customer Sets: [51397 49224 50087 24444 43554 21522] +``` + +``` +old_prop = customer.cipher.prop_key + +Customer properties: +[65030 40058 49729 42519 32475 21731] +[19446 3351 17075 17586 20753 15754] +[19712 56685 43602 30750 54931 27419] +[40397 10398 13477 26037 17943 47642] +[58359 15284 53370 4343 16407 46898] + +``` + +After the renewal, the customer properties and sets are new randomly generated values. +``` +api.renew_properties(customer_id) + +set_vals = customer.cipher.set_key + +Customer Sets: [ 7754 52659 44415 3961 61872 57312] +``` + +``` +prop_vals = customer.cipher.prop_key + +Customer properties: +[57881 51596 44681 30104 33018 30596] +[35764 62538 21274 10697 11311 42560] +[ 4979 33517 18509 55230 26674 24108] +[63335 41237 52341 30975 12398 7267] +[53495 52030 41547 59730 36417 31547] + +``` + +### Renew User +During the renewal, each user goes through a temporary transition period. +``` +props_xor = xor_lists(new_props, old_props) +sets_xor = xor_lists(new_sets, old_sets) +for user in customer.users.values(): + user.renew = True + user.user_cipher.set_key = xor_lists(user.user_cipher.set_key, sets_xor) + user.user_cipher.prop_key = xor_lists(user.user_cipher.prop_key, props_xor) +``` +##### User prop Key +The user's prop key was a randomly generated list of length `numb_of_keys * prop_per_key`. +Now each value in the prop key is `prop_key_i = old_prop_key_i ^ new_prop_i ^ old_prop_i`. +Recall in the login process, `ciphered_customer_props = prop_key ^ customer_prop`. +Since the customer_prop is now the new value, it gets canceled out, leaving: +``` +new_prop_key = old_prop_key ^ old_prop ^ new_prop +ciphered_customer_props = new_prop_key ^ new_prop +ciphered_customer_props = old_prop_key ^ old_prop # since new_prop cancel out +``` +Using the new customer properties, we can validate the user's login attempt with the same hash. + +##### User Set Key +The user's set key was a randomly generated list of length `prop_per_key` xor `customer_set_vals`. +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_cipher = UserCipherKeys.new( + customer.cipher.keypad_size, + customer.cipher.set_key, + user.user_cipher.max_nkode_len +) +user.enciphered_passcode = user.user_cipher.encipher_nkode(presumed_selected_properties_idx, customer.cipher) +user.renew = False +``` \ No newline at end of file diff --git a/docs/nkode_authentication_template.md b/docs/readme_template.md similarity index 98% rename from docs/nkode_authentication_template.md rename to docs/readme_template.md index ae7a32c..820f295 100644 --- a/docs/nkode_authentication_template.md +++ b/docs/readme_template.md @@ -1,5 +1,5 @@ -# 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. +# README +Play around with the code in /notebooks ## Customer Creation Before creating a user, a customer generates random properties and set diff --git a/docs/render_markdown.py b/docs/render_readme.py similarity index 98% rename from docs/render_markdown.py rename to docs/render_readme.py index 9472500..77c8a81 100644 --- a/docs/render_markdown.py +++ b/docs/render_readme.py @@ -33,14 +33,15 @@ def render_nkode_authentication(data: dict): env = Environment(loader=file_loader) # Load the template - template = env.get_template('nkode_authentication_template.md') + template = env.get_template('readme_template.md') print(os.getcwd()) # Render the template with the data output = template.render(data) # Print or save the output - output_file = os.path.expanduser("~/Desktop/nkode_authentication.md") + # output_file = os.path.expanduser("~/Desktop/nkode_authentication.md") + output_file = '../README.md' with open(output_file, 'w') as fp: fp.write(output) print("File written successfully") diff --git a/test/test_nkode_api.py b/test/test_nkode_api.py index 58f6f07..d2be679 100644 --- a/test/test_nkode_api.py +++ b/test/test_nkode_api.py @@ -9,16 +9,16 @@ def nkode_api() -> NKodeAPI: return NKodeAPI() -@pytest.mark.parametrize("keypad_size,passocode_len", [ +@pytest.mark.parametrize("keypad_size,passcode_len", [ (KeypadSize(numb_of_keys=10, props_per_key=11), 4), (KeypadSize(numb_of_keys=10, props_per_key=12), 5), ]) -def test_create_new_user_and_renew_keys(nkode_api, keypad_size, passocode_len): +def test_create_new_user_and_renew_keys(nkode_api, keypad_size, passcode_len): username = "test_username" nkode_policy = NKodePolicy() # default policy customer_id = nkode_api.create_new_customer(keypad_size, nkode_policy) session_id, set_keypad = nkode_api.generate_signup_keypad(customer_id) - user_passcode = set_keypad[:passocode_len] + user_passcode = set_keypad[:passcode_len] signup_key_selection = lambda keypad: [int(np.where(keypad == prop)[0][0]) // keypad_size.numb_of_keys for prop in user_passcode] set_key_selection = signup_key_selection(set_keypad)