15 KiB
nKode Authentication
Play around with the code in this jupyter notebook.
Customer Creation
Before creating a user, a customer generates random attributes and set values. The customers manage users. They define an nKode policy, keypad's dimensions, attributes/sets in the keypad, and the frequency of attribute 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 attribute and set
The keypad size defines:
- the number of keys in the keypad displayed to the user
- attributes per key
To be dispersion resistant, the number of attributes must be greater than the number of keys.
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 = {{ keypad_size.numb_of_keys }},
attrs_per_key = {{ keypad_size.attrs_per_key }} # 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 {{ keypad_size.numb_of_keys }} keys and {{ keypad_size.attrs_per_key }} attributes per key, this gives a customer interface of {{ keypad_size.numb_of_attrs }} distinct attributes and {{ keypad_size.attrs_per_key }} distinct attribute sets. Each attribute belongs to one of the {{ keypad_size.attrs_per_key }} sets. Each attribute and set value is a unique 2-byte integer in this example.
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:
- Generate a random interface
- The user sets their nKode and sends their selection to the server
- The user confirms their nKode. If the user's nKode matches the policy, the server creates the user.
Random Interface Generation
The user's interface must be dispersable so the server can determine the user's nkode. The server randomly drops attribute sets until the number of attributes equals the number of keys, making the interface dispersable. In our case, the server randomly drops {{ keypad_size.attrs_per_key - keypad_size.numb_of_keys }} attribute {{ "sets" if keypad_size.attrs_per_key - keypad_size.numb_of_keys > 1 else "set" }}. to give us a {{ keypad_size.numb_of_keys }} X {{ keypad_size.numb_of_keys }} keypad with possible index values ranging from 0-{{ keypad_size.numb_of_attrs - 1 }}. Each value in the interface is the index value of a customer attribute. The user never learns what their "real" attribute is. They do not see the index value representing their nKode or the customer server-side value.
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 be associated with the same index every time the user logs in.
If users want 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 server and receives 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 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 interface and the user's key selection. On the last api.confirm_nkode, the server:
- Deduces the user's attributes
- Validates the Passcode against the nKodePolicy
- Creates new User Cipher Keys
- Enciphers the user's mask
- 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.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 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 = {{ 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 }}
padded_passcode_server_set = user_keys.pad_user_mask(passcode_server_set, customer.nkode_policy.max_nkode_len)
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)
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
- Get login interface
- 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 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 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_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 process has three steps:
- Renew Customer Attributes
- Renew User Keys
- Refresh User on Login
When the customer calls the renew_attributes method, the method replaces the customer's attributes 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 attributes and set values are cached and copied to variables before renewal.
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 renewal, 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 renewal, 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 canceled 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
Using the new customer attributes, 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 attr_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_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