refactor nkode_authentication_template.md
This commit is contained in:
446
README.md
Normal file
446
README.md
Normal file
@@ -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:
|
||||
<ul>
|
||||
<li>the maximum length of a user's nKode</li>
|
||||
<li>the minimum length of a user's nKode</li>
|
||||
<li>the number of unique set values in a user's nKode</li>
|
||||
<li>the number of unique values in a user's nKode</li>
|
||||
<li>the number of bytes in an property and set</li>
|
||||
</ul>
|
||||
|
||||
The keypad size defines:
|
||||
<ul>
|
||||
<li>the number of keys in the keypad displayed to the user</li>
|
||||
<li>properties per key</li>
|
||||
</ul>
|
||||
|
||||
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
|
||||
```
|
||||
@@ -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
|
||||
@@ -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")
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user