2025-03-14 09:35:19 -05:00
2025-03-14 09:35:19 -05:00
2024-07-07 15:17:06 -05:00
2024-07-16 13:20:52 -05:00
2025-03-12 09:20:11 -05:00

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 maximum length of a user's nKode
  • the minimum length of a user's nKode
  • the number of unique set values in a user's nKode
  • the number of unique values in a user's nKode
  • the number of bytes in an property and set

The keypad size defines:

  • the number of keys in the keypad displayed to the user
  • properties per key

To be dispersion 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 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
Description
No description provided
Readme 311 KiB
Languages
Jupyter Notebook 60.3%
Python 39.7%