diff --git a/.obsidian/app.json b/.obsidian/app.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.obsidian/app.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.obsidian/appearance.json b/.obsidian/appearance.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/.obsidian/appearance.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/.obsidian/core-plugins.json b/.obsidian/core-plugins.json new file mode 100644 index 0000000..b977c25 --- /dev/null +++ b/.obsidian/core-plugins.json @@ -0,0 +1,31 @@ +{ + "file-explorer": true, + "global-search": true, + "switcher": true, + "graph": true, + "backlink": true, + "canvas": true, + "outgoing-link": true, + "tag-pane": true, + "properties": false, + "page-preview": true, + "daily-notes": true, + "templates": true, + "note-composer": true, + "command-palette": true, + "slash-command": false, + "editor-status": true, + "bookmarks": true, + "markdown-importer": false, + "zk-prefixer": false, + "random-note": false, + "outline": true, + "word-count": true, + "slides": false, + "audio-recorder": false, + "workspaces": false, + "file-recovery": true, + "publish": false, + "sync": true, + "webviewer": false +} \ No newline at end of file diff --git a/.obsidian/workspace.json b/.obsidian/workspace.json new file mode 100644 index 0000000..600661f --- /dev/null +++ b/.obsidian/workspace.json @@ -0,0 +1,166 @@ +{ + "main": { + "id": "2d317d71045425f3", + "type": "split", + "children": [ + { + "id": "51f85f0ef7594aef", + "type": "tabs", + "children": [ + { + "id": "60d702f2c5c78820", + "type": "leaf", + "state": { + "type": "empty", + "state": {}, + "icon": "lucide-file", + "title": "New tab" + } + } + ] + } + ], + "direction": "vertical" + }, + "left": { + "id": "933955ad281257d5", + "type": "split", + "children": [ + { + "id": "348366d432902e79", + "type": "tabs", + "children": [ + { + "id": "58e50c4fbb960df6", + "type": "leaf", + "state": { + "type": "file-explorer", + "state": { + "sortOrder": "alphabetical", + "autoReveal": false + }, + "icon": "lucide-folder-closed", + "title": "Files" + } + }, + { + "id": "db056d2cd1bd4a5b", + "type": "leaf", + "state": { + "type": "search", + "state": { + "query": "", + "matchingCase": false, + "explainSearch": false, + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical" + }, + "icon": "lucide-search", + "title": "Search" + } + }, + { + "id": "766ff21acbbfb952", + "type": "leaf", + "state": { + "type": "bookmarks", + "state": {}, + "icon": "lucide-bookmark", + "title": "Bookmarks" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300 + }, + "right": { + "id": "666e4843944fb168", + "type": "split", + "children": [ + { + "id": "7fae59c3dd546dfc", + "type": "tabs", + "children": [ + { + "id": "5a06b50e5b947981", + "type": "leaf", + "state": { + "type": "backlink", + "state": { + "collapseAll": false, + "extraContext": false, + "sortOrder": "alphabetical", + "showSearch": false, + "searchQuery": "", + "backlinkCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-coming-in", + "title": "Backlinks" + } + }, + { + "id": "57089c89592d341c", + "type": "leaf", + "state": { + "type": "outgoing-link", + "state": { + "linksCollapsed": false, + "unlinkedCollapsed": true + }, + "icon": "links-going-out", + "title": "Outgoing links" + } + }, + { + "id": "648806ec4584fa8d", + "type": "leaf", + "state": { + "type": "tag", + "state": { + "sortOrder": "frequency", + "useHierarchy": true, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-tags", + "title": "Tags" + } + }, + { + "id": "490ac34f60740511", + "type": "leaf", + "state": { + "type": "outline", + "state": { + "followCursor": false, + "showSearch": false, + "searchQuery": "" + }, + "icon": "lucide-list", + "title": "Outline" + } + } + ] + } + ], + "direction": "horizontal", + "width": 300, + "collapsed": true + }, + "left-ribbon": { + "hiddenItems": { + "switcher:Open quick switcher": false, + "graph:Open graph view": false, + "canvas:Create new canvas": false, + "daily-notes:Open today's daily note": false, + "templates:Insert template": false, + "command-palette:Open command palette": false + } + }, + "active": "60d702f2c5c78820", + "lastOpenFiles": [] +} \ No newline at end of file diff --git a/docs/encipher_decipher_renew_nkode_v2.md b/docs/encipher_decipher_renew_nkode_v2.md new file mode 100644 index 0000000..a79d9e5 --- /dev/null +++ b/docs/encipher_decipher_renew_nkode_v2.md @@ -0,0 +1,207 @@ +# Encipher, Decipher and Renew nKode + +## Tenant Policy +- max nkode length: 10 +- number of keys: 6 +- properties per key: 9 +- total number of properties: 54 + +--- + +## Deterministic CSPRNG +- AES-CTR DRBG +- ChaCha20 + +## User Cipher Keys +Derive keys in memory from PRNG above: +- property key: [ 6890 54130 42240 40467 46502 31074 10598 63689 27697 54461 21116 31999 + 10698 14870 50779 48637 29314 33075 52993 42014 2837 1935 34274 63380 + 36021 26329 20788 39848 7335 2619 61516 61122 39878 32506 19151 6611 + 2803 10730 53682 39987 11998 42378 6081 8624 34336 15222 35632 33233 + 4072 53750 54671 63845 2770 43728] +- passcode key: [10632 49355 48031 9925 15082 24190 5137 14304 24524 16141] +- user position key: [45632 9012 27470 28203 15901 7554 16974 54240 53827] +- mask key: [ 8177 54825 26281 51895 8940 16695 19756 63041 7376 54396] + +--- + +## User Keypad +- keypad example:
+- user passcode indices: [48 12 7 36] + +--- + +## nKode Cipher + +### Passcode Hash +```mermaid +block-beta + columns 2 + prop["user_property_key\n[ 6890 54130 42240 40467 46502 31074 10598 63689 27697 54461 21116 31999 + 10698 14870 50779 48637 29314 33075 52993 42014 2837 1935 34274 63380 + 36021 26329 20788 39848 7335 2619 61516 61122 39878 32506 19151 6611 + 2803 10730 53682 39987 11998 42378 6081 8624 34336 15222 35632 33233 + 4072 53750 54671 63845 2770 43728]"] + pass["user_passcode_indices\n[48 12 7 36]"] + space:2 + sel(("select\nproperties")):2 + pass --> sel + prop --> sel + space:2 + passcode["user passcode properties:\n[ 4072 10698 63689 2803]"]:2 + sel --> passcode + space:2 + pad["zero pad to\nmax nkode length: 10"]:2 + passcode -->pad + space:2 + paddedpasscode["padded passcode:\n[ 4072 10698 63689 2803 0 0 0 0 0 0]"] + pad --> paddedpasscode + passkey["passcode key:\n[10632 49355 48031 9925 15082 24190 5137 14304 24524 16141]"] + space:2 + xor2(("XOR")):2 + passkey --> xor2 + paddedpasscode --> xor2 + space:2 + cipheredpass["ciphered passcode:\n[ 9824 59649 17238 11318 15082 24190 5137 14304 24524 16141]"]:2 + xor2 --> cipheredpass + space:2 + hash(("hash")):2 + cipheredpass --> hash + space:2 + cipheredhashed["hashed ciphered passcode:\n$2b$12$XcXlcNKMyXQziv.kQWKngO88KUcm.xrn4YRZOOhGgIyMmpMw7NPJa"]:2 + hash --> cipheredhashed +``` + +### Mask Encipher +```mermaid +block-beta + columns 2 + passcode_idx["passcode indices:\n[48 12 7 36]"] + + space:3 + propidx(["Get Position Idx:\nmap each to element mod props_per_key"]) + passcode_idx-->propidx + space:3 + passcode_position_idx["passcode poition indices:\n[3, 3, 7, 0]"] + propidx --> passcode_position_idx + + space:3 + pad1(("Pad with\nrandom indices")) + passcode_position_idx --> pad1 + + space:3 + posidx["Padded Passcode Position Indices:\n[3, 3, 7, 0, 0, 0, 5, 8, 8, 8]"] + pad1 --> posidx + user_pos["user position key:\n[45632 9012 27470 28203 15901 7554 16974 54240 53827]"] + + space:2 + sel(("select positions")) + user_pos --> sel + posidx --> sel + space:3 + passcode_pos["ordered user passcode positions:\n[28203 28203 54240 45632 45632 45632 7554 53827 53827 53827]"] + sel --> passcode_pos + mask_key["mask key\n[ 8177 54825 26281 51895 8940 16695 19756 63041 7376 54396]"] + space:2 + xor2(("XOR")) + mask_key --> xor2 + passcode_pos --> xor2 + space:3 + mask["enciphered mask:\n cdq4ArVJePcB2PN3D2LrwyLN6mE="] + xor2 --> mask +``` + +### Validate nKode + +```mermaid +block-beta + columns 3 + selected_keys["keys selected by user during login:\n[1, 3, 1, 4]"] + login_keypad["login keypad:\nKey 0: [ 9 28 2 39 49 32 42 34 44] +Key 1: [ 0 1 20 48 40 50 51 7 35] +Key 2: [18 19 47 21 13 14 24 25 8] +Key 3: [27 10 11 12 4 5 6 16 17] +Key 4: [36 37 38 3 22 23 33 43 53] +Key 5: [45 46 29 30 31 41 15 52 26] +"] + space:4 + + selectkeys(("filter keys")) + mask["enciphered mask:\n cdq4ArVJePcB2PN3D2LrwyLN6mE="] + mask_key["mask key:\n[ 8177 54825 26281 51895 8940 16695 19756 63041 7376 54396]"] + space:2 + + xor1(("XOR")) + mask --> xor1 + mask_key --> xor1 + selected_keys --> selectkeys + login_keypad --> selectkeys + space:3 + + ordered_keys["ordered keys:\n[[ 0 1 20 48 40 50 51 7 35] + [27 10 11 12 4 5 6 16 17] + [ 0 1 20 48 40 50 51 7 35] + [36 37 38 3 22 23 33 43 53]]"] + user_position_key["user position key:\n[45632 9012 27470 28203 15901 7554 16974 54240 53827]"] + passcode_pos["ordered user passcode positions:\n[28203 28203 54240 45632 45632 45632 7554 53827 53827 53827]"] + selectkeys --> ordered_keys + xor1 --> passcode_pos + space:8 + + get_passcode_idxs(("recover passcode\nposition indices")) + user_position_key --> get_passcode_idxs + passcode_pos --> get_passcode_idxs + space:8 + + passcode_pos_idxs["padded passcode position indices:\n[3, 3, 7, 0, 0, 0, 5, 8, 8, 8]"] + get_passcode_idxs --> passcode_pos_idxs + space:3 + + get_presumed_idxs(("recover passcode\nproperty indices")) + ordered_keys --> get_presumed_idxs + passcode_pos_idxs --> get_presumed_idxs + space:5 + + passcode_prop_idxs["presumed passcode property indices:\n[48 12 7 36]"] + prop["user_property_key\n[ 6890 54130 42240 40467 46502 31074 10598 63689 27697 54461 21116 31999 + 10698 14870 50779 48637 29314 33075 52993 42014 2837 1935 34274 63380 + 36021 26329 20788 39848 7335 2619 61516 61122 39878 32506 19151 6611 + 2803 10730 53682 39987 11998 42378 6081 8624 34336 15222 35632 33233 + 4072 53750 54671 63845 2770 43728]"] + cipheredhashed["hashed ciphered passcode:\n$2b$12$XcXlcNKMyXQziv.kQWKngO88KUcm.xrn4YRZOOhGgIyMmpMw7NPJa"] + get_presumed_idxs --> passcode_prop_idxs + space:3 + + sel(("select\nproperties")) + passcode_prop_idxs --> sel + prop --> sel + space:5 + + passcode_prop["presumed passcode properties:\n[ 4072 10698 63689 2803]"] + sel --> passcode_prop + space:5 + + cipher(("encipher")) + passcode_prop --> cipher + space:5 + + cipheredpass["ciphered passcode:\n[ 9824 59649 17238 11318 15082 24190 5137 14304 24524 16141]"] + cipher --> cipheredpass + space:7 + + + comp{"compare"} + cipheredpass --> comp + cipheredhashed --> comp + space:5 + + suc(("success")) + comp --"Equal"--> suc +``` + +## Renew +A renewal happens every login +### Nonce Renew +With ChaCha20, we can renew the keys and hash every login with a new nonce +### Secret Renew +The secret is renewed less frequently. It's stored securely using a service like AWS Secrets Manager. diff --git a/docs/scripts/render_encipher_decipher_diagrams_v2.py b/docs/scripts/render_encipher_decipher_diagrams_v2.py new file mode 100644 index 0000000..528caa4 --- /dev/null +++ b/docs/scripts/render_encipher_decipher_diagrams_v2.py @@ -0,0 +1,69 @@ +from pathlib import Path +import numpy as np +from docs.scripts.utils import render_markdown_template +from src.models import NKodePolicy, KeypadSize +from src.user_keypad import UserKeypad +from src.nkode_cipher_v2.nkode_cipher import NKodeCipher +from src.utils import select_keys_with_passcode_values + + +def display_keypad(icons_array: np.ndarray, props_per_key: int) -> str: + icons = "" + for idx, row in enumerate(icons_array.reshape(-1, props_per_key)): + icons += f"Key {idx}: " + icons += str(row) + icons += "\n" + return icons + + +if __name__ == "__main__": + policy = NKodePolicy( + max_nkode_len=10, + min_nkode_len=4, + distinct_positions=0, + distinct_properties=4, + ) + keypad_size = KeypadSize( + numb_of_keys=6, + props_per_key=9 + ) + user_keys = NKodeCipher.create(keypad_size=keypad_size, max_nkode_len=policy.max_nkode_len) + passcode_len = 4 + + passcode_property_indices = np.random.choice([i for i in range(keypad_size.total_props)], size=passcode_len, + replace=False) + user_passcode = user_keys.property_key[passcode_property_indices] + pad_len = policy.max_nkode_len - passcode_len + padded_passcode = np.concatenate((user_passcode, np.zeros(pad_len, dtype=user_passcode.dtype))) + ciphered_passcode = padded_passcode ^ user_keys.pass_key + cipher = user_keys.encipher_nkode(passcode_property_indices.tolist()) + padded_passcode_position_indices = user_keys.get_passcode_position_indices_padded( + passcode_property_indices.tolist()) + + ordered_user_position_key = user_keys.position_key[padded_passcode_position_indices] + login_keypad = UserKeypad.create(keypad_size) + selected_keys_login = select_keys_with_passcode_values(passcode_property_indices, login_keypad.keypad, + keypad_size.props_per_key) + context = { + "max_nkode_len": policy.max_nkode_len, + "numb_of_keys": keypad_size.numb_of_keys, + "props_per_key": keypad_size.props_per_key, + "user_property_key": user_keys.property_key, + "pass_key": user_keys.pass_key, + "user_position_key": user_keys.position_key, + "mask_key": user_keys.mask_key, + "user_passcode_idxs": passcode_property_indices, + "user_passcode_props": user_passcode, + "padded_passcode": padded_passcode, + "ciphered_passcode": ciphered_passcode, + "code": cipher.code, + "passcode_position_idxs": padded_passcode_position_indices[:passcode_len], + "pad_user_passcode_idxs": padded_passcode_position_indices, + "ordered_user_position_key": ordered_user_position_key, + "mask": cipher.mask, + "login_keypad": display_keypad(login_keypad.keypad, keypad_size.props_per_key), + "selected_keys": selected_keys_login, + "ordered_keys": login_keypad.keypad.reshape(-1, keypad_size.props_per_key)[selected_keys_login], + } + render_markdown_template(Path("../templates/encipher_decipher_renew_nkode_v2.template.md"), + Path("../encipher_decipher_renew_nkode_v2.md"), context) diff --git a/docs/scripts/render_zero_trust_nkode.py b/docs/scripts/render_zero_trust_nkode.py new file mode 100644 index 0000000..1b471ed --- /dev/null +++ b/docs/scripts/render_zero_trust_nkode.py @@ -0,0 +1,70 @@ +from pathlib import Path +import numpy as np +from docs.scripts.utils import render_markdown_template +from src.models import NKodePolicy, KeypadSize +from src.user_keypad import UserKeypad +from src.nkode_cipher_v2.nkode_cipher import NKodeCipher +from src.utils import select_keys_with_passcode_values + + +def display_keypad(icons_array: np.ndarray, props_per_key: int) -> str: + icons = "" + for idx, row in enumerate(icons_array.reshape(-1, props_per_key)): + icons += f"Key {idx}: " + icons += str(row) + icons += "\n" + return icons + + +if __name__ == "__main__": + policy = NKodePolicy( + max_nkode_len=10, + min_nkode_len=4, + distinct_positions=0, + distinct_properties=4, + ) + keypad_size = KeypadSize( + numb_of_keys=6, + props_per_key=9 + ) + user_keys = NKodeCipher.create(keypad_size=keypad_size, max_nkode_len=policy.max_nkode_len) + passcode_len = 4 + + passcode_property_indices = np.random.choice([i for i in range(keypad_size.total_props)], size=passcode_len, + replace=False) + user_passcode = user_keys.property_key[passcode_property_indices] + pad_len = policy.max_nkode_len - passcode_len + padded_passcode = np.concatenate((user_passcode, np.zeros(pad_len, dtype=user_passcode.dtype))) + ciphered_passcode = padded_passcode ^ user_keys.pass_key + cipher = user_keys.encipher_nkode(passcode_property_indices.tolist()) + padded_passcode_position_indices = user_keys.get_passcode_position_indices_padded( + passcode_property_indices.tolist()) + + ordered_user_position_key = user_keys.position_key[padded_passcode_position_indices] + login_keypad = UserKeypad.create(keypad_size) + selected_keys_login = select_keys_with_passcode_values(passcode_property_indices, login_keypad.keypad, + keypad_size.props_per_key) + context = { + "max_nkode_len": policy.max_nkode_len, + "numb_of_keys": keypad_size.numb_of_keys, + "props_per_key": keypad_size.props_per_key, + "user_property_key": user_keys.property_key, + "pass_key": user_keys.pass_key, + "user_position_key": user_keys.position_key, + "mask_key": user_keys.mask_key, + "user_passcode_idxs": passcode_property_indices, + "user_passcode_props": user_passcode, + "padded_passcode": padded_passcode, + "ciphered_passcode": ciphered_passcode, + "code": cipher.code, + "passcode_position_idxs": padded_passcode_position_indices[:passcode_len], + "pad_user_passcode_idxs": padded_passcode_position_indices, + "ordered_user_position_key": ordered_user_position_key, + "mask": cipher.mask, + "login_keypad": display_keypad(login_keypad.keypad, keypad_size.props_per_key), + "selected_keys": selected_keys_login, + "ordered_keys": login_keypad.keypad.reshape(-1, keypad_size.props_per_key)[selected_keys_login], + } + + render_markdown_template(Path("../templates/zero_trust_nkode.template.md"), + Path("../zero_trust_nkode.md"), context) diff --git a/docs/templates/encipher_decipher_renew_nkode_v2.template.md b/docs/templates/encipher_decipher_renew_nkode_v2.template.md new file mode 100644 index 0000000..c50668a --- /dev/null +++ b/docs/templates/encipher_decipher_renew_nkode_v2.template.md @@ -0,0 +1,187 @@ +# Encipher, Decipher and Renew nKode + +## Tenant Policy +- max nkode length: {{ max_nkode_len }} +- number of keys: {{ numb_of_keys }} +- properties per key: {{ props_per_key }} +- total number of properties: {{ numb_of_keys * props_per_key }} + +--- + +## Deterministic CSPRNG +- AES-CTR DRBG +- ChaCha20 + +## User Cipher Keys +Derive keys in memory from PRNG above: +- property key: {{ user_property_key }} +- passcode key: {{ pass_key }} +- user position key: {{ user_position_key }} +- mask key: {{ mask_key }} + +--- + +## User Keypad +- keypad example:
{{ login_keypad_md }} +- user passcode indices: {{ user_passcode_idxs}} + +--- + +## nKode Cipher + +### Passcode Hash +```mermaid +block-beta + columns 2 + prop["user_property_key\n{{user_property_key}}"] + pass["user_passcode_indices\n{{user_passcode_idxs}}"] + space:2 + sel(("select\nproperties")):2 + pass --> sel + prop --> sel + space:2 + passcode["user passcode properties:\n{{user_passcode_props}}"]:2 + sel --> passcode + space:2 + pad["zero pad to\nmax nkode length: {{max_nkode_len}}"]:2 + passcode -->pad + space:2 + paddedpasscode["padded passcode:\n{{padded_passcode}}"] + pad --> paddedpasscode + passkey["passcode key:\n{{pass_key}}"] + space:2 + xor2(("XOR")):2 + passkey --> xor2 + paddedpasscode --> xor2 + space:2 + cipheredpass["ciphered passcode:\n{{ciphered_passcode}}"]:2 + xor2 --> cipheredpass + space:2 + hash(("hash")):2 + cipheredpass --> hash + space:2 + cipheredhashed["hashed ciphered passcode:\n{{code}}"]:2 + hash --> cipheredhashed +``` + +### Mask Encipher +```mermaid +block-beta + columns 2 + passcode_idx["passcode indices:\n{{user_passcode_idxs}}"] + + space:3 + propidx(["Get Position Idx:\nmap each to element mod props_per_key"]) + passcode_idx-->propidx + space:3 + passcode_position_idx["passcode poition indices:\n{{passcode_position_idxs}}"] + propidx --> passcode_position_idx + + space:3 + pad1(("Pad with\nrandom indices")) + passcode_position_idx --> pad1 + + space:3 + posidx["Padded Passcode Position Indices:\n{{pad_user_passcode_idxs}}"] + pad1 --> posidx + user_pos["user position key:\n{{user_position_key}}"] + + space:2 + sel(("select positions")) + user_pos --> sel + posidx --> sel + space:3 + passcode_pos["ordered user passcode positions:\n{{ordered_user_position_key}}"] + sel --> passcode_pos + mask_key["mask key\n{{mask_key}}"] + space:2 + xor2(("XOR")) + mask_key --> xor2 + passcode_pos --> xor2 + space:3 + mask["enciphered mask:\n {{mask}}"] + xor2 --> mask +``` + +### Validate nKode + +```mermaid +block-beta + columns 3 + selected_keys["keys selected by user during login:\n{{selected_keys}}"] + login_keypad["login keypad:\n{{login_keypad}}"] + space:4 + + selectkeys(("filter keys")) + mask["enciphered mask:\n {{mask}}"] + mask_key["mask key:\n{{mask_key}}"] + space:2 + + xor1(("XOR")) + mask --> xor1 + mask_key --> xor1 + selected_keys --> selectkeys + login_keypad --> selectkeys + space:3 + + ordered_keys["ordered keys:\n{{ordered_keys}}"] + user_position_key["user position key:\n{{user_position_key}}"] + passcode_pos["ordered user passcode positions:\n{{ordered_user_position_key}}"] + selectkeys --> ordered_keys + xor1 --> passcode_pos + space:8 + + get_passcode_idxs(("recover passcode\nposition indices")) + user_position_key --> get_passcode_idxs + passcode_pos --> get_passcode_idxs + space:8 + + passcode_pos_idxs["padded passcode position indices:\n{{pad_user_passcode_idxs}}"] + get_passcode_idxs --> passcode_pos_idxs + space:3 + + get_presumed_idxs(("recover passcode\nproperty indices")) + ordered_keys --> get_presumed_idxs + passcode_pos_idxs --> get_presumed_idxs + space:5 + + passcode_prop_idxs["presumed passcode property indices:\n{{user_passcode_idxs}}"] + prop["user_property_key\n{{user_property_key}}"] + cipheredhashed["hashed ciphered passcode:\n{{code}}"] + get_presumed_idxs --> passcode_prop_idxs + space:3 + + sel(("select\nproperties")) + passcode_prop_idxs --> sel + prop --> sel + space:5 + + passcode_prop["presumed passcode properties:\n{{user_passcode_props}}"] + sel --> passcode_prop + space:5 + + cipher(("encipher")) + passcode_prop --> cipher + space:5 + + cipheredpass["ciphered passcode:\n{{ciphered_passcode}}"] + cipher --> cipheredpass + space:7 + + + comp{"compare"} + cipheredpass --> comp + cipheredhashed --> comp + space:5 + + suc(("success")) + comp --"Equal"--> suc +``` + +## Renew +A renewal happens every login +### Nonce Renew +With ChaCha20, we can renew the keys and hash every login with a new nonce +### Secret Renew +The secret is renewed less frequently. It's stored securely using a service like AWS Secrets Manager. + diff --git a/docs/templates/zero_trust_nkode.template.md b/docs/templates/zero_trust_nkode.template.md new file mode 100644 index 0000000..4370270 --- /dev/null +++ b/docs/templates/zero_trust_nkode.template.md @@ -0,0 +1,43 @@ +# Zero Trust nKode with aPAKE (OPAQUE) + +```mermaid +sequenceDiagram + participant Client + participant Server + Note over Client, Server: Enrollment + Client ->> Server: Signup Session: email + Client ->> Client: Create 128-bit Secret Key + Note left of Client: Request user stores Secret Key in a safe place + Client ->> Server: OPAQUE Register with Secret Key
https://github.com/facebook/opaque-ke + Client ->> Server: OPAQUE Login with email + Secret Key + opt Secret Key OPAQUE tunnel + Client ->> Server: Get New Icons + Server -->> Client: icons + Note left of Client: Icons are stored on Client + Note left of Client: well-known nonce: 0x1 (or any number) + Client ->> Client: Assign random names to icons from
secret_key and well known nonce + Client ->> Server: list of random icon names + Note right of Server: Only a client with the secret key can request these icons.
Server doesn't know the owner + loop assign icons + Client ->> Client: Regenerate 4-6 icons until user accepts them + end + + Client ->> Client: Create new nonce + Client ->> Client: ChaCha20 key derivation (pass_key, mask_key, prop_key, pos_key) + Client ->> Client: Compute Mask + Note left of Client: User Password is concat([list of assigned icon values]) + Client ->> Server: OPAQUE Register with User Password + nonce, mask + end + Note over Client, Server: Login + Client ->> Server: OPAQUE Login with email + Secret Key + opt Secret Key OPAQUE tunnel + Server ->> Client: nonce, mask + Client ->> Client: Display Keypad to User
User makes key selection + Client ->> Client: recover user password + Client ->> Server: OPAQUE Password Login + end + Note over Client, Server: User Session + opt Secret Key PAKE Key XOR nKode PAKE Key tunnel + Client ->> Server: all communication goes through this double PAKE + end +``` diff --git a/notebooks/Enrollment_Login_Renewal_Detailed.ipynb b/notebooks/Enrollment_Login_Renewal_Detailed.ipynb index 6fad737..e611d51 100644 --- a/notebooks/Enrollment_Login_Renewal_Detailed.ipynb +++ b/notebooks/Enrollment_Login_Renewal_Detailed.ipynb @@ -2,6 +2,18 @@ "cells": [ { "cell_type": "code", + "execution_count": 1, + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-27T19:17:57.439685Z", + "start_time": "2025-03-27T19:17:57.405237Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], "source": [ "import sys\n", "import os\n", @@ -25,25 +37,18 @@ " interface_keypad = keypad.reshape(-1, props_per_key)\n", " for idx, key_vals in enumerate(interface_keypad):\n", " print(f\"Key {idx}: {key_vals}\")\n" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2025-03-27T19:17:57.439685Z", - "start_time": "2025-03-27T19:17:57.405237Z" - } - }, - "outputs": [], - "execution_count": 1 + ] }, { + "cell_type": "code", + "execution_count": 2, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:57.446190Z", "start_time": "2025-03-27T19:17:57.443952Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "api = NKodeAPI()\n", "user_icons = np.array([\n", @@ -54,13 +59,11 @@ " \"🦁\", \"🐻\", \"🐸\", \"🐙\", \"🦄\",\n", " \"🌟\", \"⚡\", \"🔥\", \"🍕\", \"🎉\"\n", "])" - ], - "outputs": [], - "execution_count": 2 + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "### nKode Customer\n", "An nKode customer is business has employees (users). An nKode API can service many customers each with their own users.\n", @@ -69,8 +72,8 @@ ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "#### Customer Cipher Keys\n", "Each customer has unique cipher keys.\n", @@ -81,36 +84,14 @@ ] }, { + "cell_type": "code", + "execution_count": 3, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:57.487136Z", "start_time": "2025-03-27T19:17:57.475079Z" } }, - "cell_type": "code", - "source": [ - "policy = NKodePolicy(\n", - " max_nkode_len=10,\n", - " min_nkode_len=4,\n", - " distinct_positions=0,\n", - " distinct_properties=4,\n", - ")\n", - "keypad_size = KeypadSize(\n", - " numb_of_keys = 5,\n", - " props_per_key = 6\n", - ")\n", - "customer_id = api.create_new_customer(keypad_size, policy)\n", - "customer = api.customers[customer_id]\n", - "print(f\"Customer Position Key: {customer.cipher.position_key}\")\n", - "print(f\"Customer Properties Key:\")\n", - "customer_prop_keypad = customer.cipher.property_key.reshape(-1, keypad_size.props_per_key)\n", - "for idx, key_vals in enumerate(customer_prop_keypad):\n", - " print(f\"{key_vals}\")\n", - "position_properties_dict = dict(zip(customer.cipher.position_key, customer_prop_keypad.T))\n", - "print(f\"Position to Properties Map:\")\n", - "for pos_val, props in position_properties_dict.items():\n", - " print(f\"{pos_val}: {props}\")" - ], "outputs": [ { "name": "stdout", @@ -133,23 +114,39 @@ ] } ], - "execution_count": 3 + "source": [ + "policy = NKodePolicy(\n", + " max_nkode_len=10,\n", + " min_nkode_len=4,\n", + " distinct_positions=0,\n", + " distinct_properties=4,\n", + ")\n", + "keypad_size = KeypadSize(\n", + " numb_of_keys = 5,\n", + " props_per_key = 6\n", + ")\n", + "customer_id = api.create_new_customer(keypad_size, policy)\n", + "customer = api.customers[customer_id]\n", + "print(f\"Customer Position Key: {customer.cipher.position_key}\")\n", + "print(f\"Customer Properties Key:\")\n", + "customer_prop_keypad = customer.cipher.property_key.reshape(-1, keypad_size.props_per_key)\n", + "for idx, key_vals in enumerate(customer_prop_keypad):\n", + " print(f\"{key_vals}\")\n", + "position_properties_dict = dict(zip(customer.cipher.position_key, customer_prop_keypad.T))\n", + "print(f\"Position to Properties Map:\")\n", + "for pos_val, props in position_properties_dict.items():\n", + " print(f\"{pos_val}: {props}\")" + ] }, { + "cell_type": "code", + "execution_count": 4, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:57.507904Z", "start_time": "2025-03-27T19:17:57.505187Z" } }, - "cell_type": "code", - "source": [ - "user_icon_keypad = user_icons.reshape(-1, keypad_size.props_per_key)\n", - "pos_icons_dict = dict(zip(customer.cipher.position_key, user_icon_keypad.T))\n", - "print(\"Position Value to Icons Map:\")\n", - "for pos_val, icons in pos_icons_dict.items():\n", - " print(f\"{pos_val}: {icons}\")\n" - ], "outputs": [ { "name": "stdout", @@ -165,11 +162,17 @@ ] } ], - "execution_count": 4 + "source": [ + "user_icon_keypad = user_icons.reshape(-1, keypad_size.props_per_key)\n", + "pos_icons_dict = dict(zip(customer.cipher.position_key, user_icon_keypad.T))\n", + "print(\"Position Value to Icons Map:\")\n", + "for pos_val, icons in pos_icons_dict.items():\n", + " print(f\"{pos_val}: {icons}\")\n" + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "### User Signup\n", "Users can create an nKode with these steps:\n", @@ -187,30 +190,23 @@ ] }, { + "cell_type": "code", + "execution_count": 5, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:57.541997Z", "start_time": "2025-03-27T19:17:57.534379Z" } }, - "cell_type": "code", - "source": [ - "username = random_username()\n", - "signup_session_id, set_signup_keypad = api.generate_signup_keypad(customer_id, username)\n", - "display(Markdown(\"\"\"### Icon Keypad\"\"\"))\n", - "keypad_view(user_icons[set_signup_keypad], keypad_size.numb_of_keys)\n", - "display(Markdown(\"\"\"### Index Keypad\"\"\"))\n", - "keypad_view(set_signup_keypad, keypad_size.numb_of_keys)\n", - "display(Markdown(\"\"\"### Customer Properties Keypad\"\"\"))\n", - "keypad_view(customer.cipher.property_key[set_signup_keypad], keypad_size.numb_of_keys)" - ], "outputs": [ { "data": { + "text/markdown": [ + "### Icon Keypad" + ], "text/plain": [ "" - ], - "text/markdown": "### Icon Keypad" + ] }, "metadata": {}, "output_type": "display_data" @@ -228,10 +224,12 @@ }, { "data": { + "text/markdown": [ + "### Index Keypad" + ], "text/plain": [ "" - ], - "text/markdown": "### Index Keypad" + ] }, "metadata": {}, "output_type": "display_data" @@ -249,10 +247,12 @@ }, { "data": { + "text/markdown": [ + "### Customer Properties Keypad" + ], "text/plain": [ "" - ], - "text/markdown": "### Customer Properties Keypad" + ] }, "metadata": {}, "output_type": "display_data" @@ -269,33 +269,34 @@ ] } ], - "execution_count": 5 + "source": [ + "username = random_username()\n", + "signup_session_id, set_signup_keypad = api.generate_signup_keypad(customer_id, username)\n", + "display(Markdown(\"\"\"### Icon Keypad\"\"\"))\n", + "keypad_view(user_icons[set_signup_keypad], keypad_size.numb_of_keys)\n", + "display(Markdown(\"\"\"### Index Keypad\"\"\"))\n", + "keypad_view(set_signup_keypad, keypad_size.numb_of_keys)\n", + "display(Markdown(\"\"\"### Customer Properties Keypad\"\"\"))\n", + "keypad_view(customer.cipher.property_key[set_signup_keypad], keypad_size.numb_of_keys)" + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "### Set nKode\n", "The client receives `user_icons`, `set_signup_keypad`\n" ] }, { + "cell_type": "code", + "execution_count": 6, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:57.582109Z", "start_time": "2025-03-27T19:17:57.578783Z" } }, - "cell_type": "code", - "source": [ - "passcode_len = 4\n", - "passcode_property_indices = np.random.choice(set_signup_keypad.reshape(-1), size=passcode_len, replace=False).tolist()\n", - "selected_keys_set = select_keys_with_passcode_values(passcode_property_indices, set_signup_keypad, keypad_size.numb_of_keys)\n", - "print(f\"User Passcode Indices: {passcode_property_indices}\")\n", - "print(f\"User Passcode Icons: {user_icons[passcode_property_indices]}\")\n", - "print(f\"User Passcode Server-side properties: {customer.cipher.property_key[passcode_property_indices]}\")\n", - "print(f\"Selected Keys: {selected_keys_set}\")" - ], "outputs": [ { "name": "stdout", @@ -308,32 +309,33 @@ ] } ], - "execution_count": 6 + "source": [ + "passcode_len = 4\n", + "passcode_property_indices = np.random.choice(set_signup_keypad.reshape(-1), size=passcode_len, replace=False).tolist()\n", + "selected_keys_set = select_keys_with_passcode_values(passcode_property_indices, set_signup_keypad, keypad_size.numb_of_keys)\n", + "print(f\"User Passcode Indices: {passcode_property_indices}\")\n", + "print(f\"User Passcode Icons: {user_icons[passcode_property_indices]}\")\n", + "print(f\"User Passcode Server-side properties: {customer.cipher.property_key[passcode_property_indices]}\")\n", + "print(f\"Selected Keys: {selected_keys_set}\")" + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "### Confirm nKode\n", "Submit the set key entry to render the confirm keypad." ] }, { + "cell_type": "code", + "execution_count": 7, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:57.846195Z", "start_time": "2025-03-27T19:17:57.599899Z" } }, - "cell_type": "code", - "source": [ - "confirm_keypad = api.set_nkode(customer_id, selected_keys_set, signup_session_id)\n", - "keypad_view(confirm_keypad, keypad_size.numb_of_keys)\n", - "selected_keys_confirm = select_keys_with_passcode_values(passcode_property_indices, confirm_keypad, keypad_size.numb_of_keys)\n", - "print(f\"Selected Keys\\n{selected_keys_confirm}\")\n", - "success = api.confirm_nkode(customer_id, selected_keys_confirm, signup_session_id)\n", - "assert success" - ], "outputs": [ { "name": "stdout", @@ -349,31 +351,31 @@ ] } ], - "execution_count": 7 + "source": [ + "confirm_keypad = api.set_nkode(customer_id, selected_keys_set, signup_session_id)\n", + "keypad_view(confirm_keypad, keypad_size.numb_of_keys)\n", + "selected_keys_confirm = select_keys_with_passcode_values(passcode_property_indices, confirm_keypad, keypad_size.numb_of_keys)\n", + "print(f\"Selected Keys\\n{selected_keys_confirm}\")\n", + "success = api.confirm_nkode(customer_id, selected_keys_confirm, signup_session_id)\n", + "assert success" + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "### Inferring an nKode selection" + "metadata": {}, + "source": [ + "### Inferring an nKode selection" + ] }, { + "cell_type": "code", + "execution_count": 8, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:57.858315Z", "start_time": "2025-03-27T19:17:57.855013Z" } }, - "cell_type": "code", - "source": [ - "for idx in range(passcode_len):\n", - " selected_key_set = selected_keys_set[idx]\n", - " selected_set_key_idx = set_signup_keypad.reshape(-1, keypad_size.numb_of_keys)[selected_key_set]\n", - " print(f\"Set Key {idx}: {user_icons[selected_set_key_idx]}\")\n", - " selected_key_confirm = selected_keys_confirm[idx]\n", - " selected_confirm_key_idx = confirm_keypad.reshape(-1, keypad_size.numb_of_keys)[selected_key_confirm]\n", - " print(f\"Confirm Key {idx}: {user_icons[selected_confirm_key_idx]}\")\n", - " print(f\"Overlapping icon {user_icons[passcode_property_indices[idx]]}\")" - ], "outputs": [ { "name": "stdout", @@ -394,11 +396,20 @@ ] } ], - "execution_count": 8 + "source": [ + "for idx in range(passcode_len):\n", + " selected_key_set = selected_keys_set[idx]\n", + " selected_set_key_idx = set_signup_keypad.reshape(-1, keypad_size.numb_of_keys)[selected_key_set]\n", + " print(f\"Set Key {idx}: {user_icons[selected_set_key_idx]}\")\n", + " selected_key_confirm = selected_keys_confirm[idx]\n", + " selected_confirm_key_idx = confirm_keypad.reshape(-1, keypad_size.numb_of_keys)[selected_key_confirm]\n", + " print(f\"Confirm Key {idx}: {user_icons[selected_confirm_key_idx]}\")\n", + " print(f\"Overlapping icon {user_icons[passcode_property_indices[idx]]}\")" + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "## User Cipher\n", "\n", @@ -412,30 +423,30 @@ ] }, { + "cell_type": "code", + "execution_count": 9, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:57.879460Z", "start_time": "2025-03-27T19:17:57.873816Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "from src.user_cipher import UserCipher\n", "user_cipher = UserCipher.create(keypad_size, customer.cipher.position_key, customer.nkode_policy.max_nkode_len)\n", "user_prop_key_keypad = user_cipher.property_key.reshape(-1, keypad_size.props_per_key)" - ], - "outputs": [], - "execution_count": 9 + ] }, { + "cell_type": "code", + "execution_count": 10, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:57.904749Z", "start_time": "2025-03-27T19:17:57.902224Z" } }, - "cell_type": "code", - "source": "print(f\"Property Key:\\n{user_prop_key_keypad}\")", "outputs": [ { "name": "stdout", @@ -450,17 +461,19 @@ ] } ], - "execution_count": 10 + "source": [ + "print(f\"Property Key:\\n{user_prop_key_keypad}\")" + ] }, { + "cell_type": "code", + "execution_count": 11, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:57.951642Z", "start_time": "2025-03-27T19:17:57.949347Z" } }, - "cell_type": "code", - "source": "print(f\"Passcode Key: {user_cipher.pass_key}\")", "outputs": [ { "name": "stdout", @@ -470,17 +483,19 @@ ] } ], - "execution_count": 11 + "source": [ + "print(f\"Passcode Key: {user_cipher.pass_key}\")" + ] }, { + "cell_type": "code", + "execution_count": 12, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:57.989194Z", "start_time": "2025-03-27T19:17:57.986687Z" } }, - "cell_type": "code", - "source": "print(f\"Mask Key: {user_cipher.mask_key}\")", "outputs": [ { "name": "stdout", @@ -490,17 +505,19 @@ ] } ], - "execution_count": 12 + "source": [ + "print(f\"Mask Key: {user_cipher.mask_key}\")" + ] }, { + "cell_type": "code", + "execution_count": 13, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:58.019886Z", "start_time": "2025-03-27T19:17:58.017350Z" } }, - "cell_type": "code", - "source": "print(f\"Combined Position Key: {user_cipher.combined_position_key}\")", "outputs": [ { "name": "stdout", @@ -510,17 +527,19 @@ ] } ], - "execution_count": 13 + "source": [ + "print(f\"Combined Position Key: {user_cipher.combined_position_key}\")" + ] }, { + "cell_type": "code", + "execution_count": 14, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:58.050384Z", "start_time": "2025-03-27T19:17:58.047868Z" } }, - "cell_type": "code", - "source": "print(f\"User Position Key = combined_pos_key XOR customer_pos_key: {user_cipher.combined_position_key ^ customer.cipher.position_key}\")", "outputs": [ { "name": "stdout", @@ -530,22 +549,19 @@ ] } ], - "execution_count": 14 + "source": [ + "print(f\"User Position Key = combined_pos_key XOR customer_pos_key: {user_cipher.combined_position_key ^ customer.cipher.position_key}\")" + ] }, { + "cell_type": "code", + "execution_count": 15, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:58.138360Z", "start_time": "2025-03-27T19:17:58.135782Z" } }, - "cell_type": "code", - "source": [ - "position_properties_dict = dict(zip(user_cipher.combined_position_key, user_prop_key_keypad.T))\n", - "print(f\"Combined Position to Properties Map:\")\n", - "for pos_val, props in position_properties_dict.items():\n", - " print(f\"{pos_val}: {props}\")" - ], "outputs": [ { "name": "stdout", @@ -561,11 +577,16 @@ ] } ], - "execution_count": 15 + "source": [ + "position_properties_dict = dict(zip(user_cipher.combined_position_key, user_prop_key_keypad.T))\n", + "print(f\"Combined Position to Properties Map:\")\n", + "for pos_val, props in position_properties_dict.items():\n", + " print(f\"{pos_val}: {props}\")" + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "#### Encipher Mask\n", "1. Get the `padded_passcode_position_indices`; padded with random position indices to equal length `max_nkode_len`.\n", @@ -576,26 +597,26 @@ ] }, { + "cell_type": "code", + "execution_count": 16, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:58.179247Z", "start_time": "2025-03-27T19:17:58.176595Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "padded_passcode_position_indices = customer.cipher.get_passcode_position_indices_padded(list(passcode_property_indices), customer.nkode_policy.max_nkode_len)\n", "user_position_key = user_cipher.combined_position_key ^ customer.cipher.position_key\n", "ordered_user_position_key = user_position_key[padded_passcode_position_indices]\n", "mask = ordered_user_position_key ^ user_cipher.mask_key\n", "encoded_mask = user_cipher.encode_base64_str(mask)" - ], - "outputs": [], - "execution_count": 16 + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "#### Encipher Passcode\n", "1. Compute `combined_property_key`\n", @@ -606,13 +627,15 @@ ] }, { + "cell_type": "code", + "execution_count": 17, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:58.455536Z", "start_time": "2025-03-27T19:17:58.212205Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "combined_prop_key = customer.cipher.property_key ^ user_cipher.property_key\n", "user_passcode = combined_prop_key[passcode_property_indices]\n", @@ -621,13 +644,11 @@ "ciphered_passcode = padded_passcode ^ user_cipher.pass_key\n", "passcode_prehash = base64.b64encode(hashlib.sha256(ciphered_passcode.tobytes()).digest())\n", "passcode_hash = bcrypt.hashpw(passcode_prehash, bcrypt.gensalt(rounds=12)).decode(\"utf-8\")" - ], - "outputs": [], - "execution_count": 17 + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "### User Login\n", "1. Get login keypad\n", @@ -635,22 +656,14 @@ ] }, { + "cell_type": "code", + "execution_count": 18, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:58.702891Z", "start_time": "2025-03-27T19:17:58.461555Z" } }, - "cell_type": "code", - "source": [ - "login_keypad = api.get_login_keypad(username, customer_id)\n", - "keypad_view(login_keypad, keypad_size.props_per_key)\n", - "selected_keys_login = select_keys_with_passcode_values(passcode_property_indices, login_keypad, keypad_size.props_per_key)\n", - "print(f\"User Passcode: {passcode_property_indices}\\n\")\n", - "print(f\"Selected Keys:\\n {selected_keys_login}\\n\")\n", - "success = api.login(customer_id, username, selected_keys_login)\n", - "assert success" - ], "outputs": [ { "name": "stdout", @@ -669,11 +682,19 @@ ] } ], - "execution_count": 18 + "source": [ + "login_keypad = api.get_login_keypad(username, customer_id)\n", + "keypad_view(login_keypad, keypad_size.props_per_key)\n", + "selected_keys_login = select_keys_with_passcode_values(passcode_property_indices, login_keypad, keypad_size.props_per_key)\n", + "print(f\"User Passcode: {passcode_property_indices}\\n\")\n", + "print(f\"Selected Keys:\\n {selected_keys_login}\\n\")\n", + "success = api.login(customer_id, username, selected_keys_login)\n", + "assert success" + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "## Validate Login Key Entry\n", "- decipher user mask and recover nkode position values\n", @@ -682,8 +703,8 @@ ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "### Decipher Mask\n", "Recover nKode position values:\n", @@ -694,13 +715,15 @@ ] }, { + "cell_type": "code", + "execution_count": 19, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:58.713231Z", "start_time": "2025-03-27T19:17:58.710458Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "login_keypad = api.get_login_keypad(username, customer_id)\n", "selected_keys_login = select_keys_with_passcode_values(passcode_property_indices, login_keypad, keypad_size.props_per_key)\n", @@ -708,13 +731,11 @@ "mask = user.cipher.decode_base64_str(user.enciphered_passcode.mask)\n", "ordered_user_position_key = mask ^ user.cipher.mask_key\n", "user_position_key = customer.cipher.position_key ^ user.cipher.combined_position_key" - ], - "outputs": [], - "execution_count": 19 + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "#### Get Presumed Properties\n", "- Get the passcode position indices (within the keys)\n", @@ -722,44 +743,46 @@ ] }, { + "cell_type": "code", + "execution_count": 20, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:58.729425Z", "start_time": "2025-03-27T19:17:58.727025Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "passcode_position_indices = [int(np.where(user_position_key == pos)[0][0]) for pos in ordered_user_position_key[:passcode_len]]\n", "presumed_property_indices = customer.users[username].user_keypad.get_prop_idxs_by_keynumb_setidx(selected_keys_login, passcode_position_indices)\n", "assert passcode_property_indices == presumed_property_indices\n" - ], - "outputs": [], - "execution_count": 20 + ] }, { - "metadata": {}, "cell_type": "markdown", - "source": "### Compare Enciphered Passcodes\n" + "metadata": {}, + "source": [ + "### Compare Enciphered Passcodes\n" + ] }, { + "cell_type": "code", + "execution_count": 21, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:58.983936Z", "start_time": "2025-03-27T19:17:58.739679Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "valid_nkode = user.cipher.compare_nkode(presumed_property_indices, customer.cipher, user.enciphered_passcode.code)\n", "assert valid_nkode" - ], - "outputs": [], - "execution_count": 21 + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "## Renew Properties\n", "1. Renew Customer Keys\n", @@ -769,13 +792,28 @@ ] }, { + "cell_type": "code", + "execution_count": 22, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:59.477909Z", "start_time": "2025-03-27T19:17:58.990632Z" } }, - "cell_type": "code", + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Old User Cipher and Mask\n", + "mask: DUY6OIJHwzgG5ajVEGKHARHM6So=, code: $2b$12$/Za40kT7mC0quZxMUWCDs.4cF.3r2meCUBEoz0EWlSKkJAMOPiJTy\n", + "\n", + "New User Cipher and Mask\n", + "mask: GGBaMjr+zlPhS+pmfstihUeFt6A=, code: $2b$12$wE7bq7sYd8Q58j.qKS5ASO2IMzaJ71UW/0vOYAhx7zUhURDJYIaZi\n", + "\n" + ] + } + ], "source": [ "def print_user_enciphered_code():\n", " mask = api.customers[customer_id].users[username].enciphered_passcode.mask\n", @@ -791,39 +829,26 @@ "print(\"New User Cipher and Mask\")\n", "print_user_enciphered_code()\n", "assert success" - ], - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Old User Cipher and Mask\n", - "mask: DUY6OIJHwzgG5ajVEGKHARHM6So=, code: $2b$12$/Za40kT7mC0quZxMUWCDs.4cF.3r2meCUBEoz0EWlSKkJAMOPiJTy\n", - "\n", - "New User Cipher and Mask\n", - "mask: GGBaMjr+zlPhS+pmfstihUeFt6A=, code: $2b$12$wE7bq7sYd8Q58j.qKS5ASO2IMzaJ71UW/0vOYAhx7zUhURDJYIaZi\n", - "\n" - ] - } - ], - "execution_count": 22 + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "### Renew Customer Keys\n", "The customer cipher keys are replaced." ] }, { + "cell_type": "code", + "execution_count": 23, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:59.490134Z", "start_time": "2025-03-27T19:17:59.486082Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "old_props = customer.cipher.property_key.copy()\n", "old_pos = customer.cipher.position_key.copy()\n", @@ -831,13 +856,11 @@ "customer.cipher.position_key = np.random.choice(2 ** 16, size=keypad_size.props_per_key, replace=False)\n", "new_props = customer.cipher.property_key\n", "new_pos = customer.cipher.position_key" - ], - "outputs": [], - "execution_count": 23 + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "### Intermediate Renew User\n", "User property and position keys go through an intermediate phase.\n", @@ -851,13 +874,15 @@ ] }, { + "cell_type": "code", + "execution_count": 24, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:59.500256Z", "start_time": "2025-03-27T19:17:59.497839Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "props_xor = new_props ^ old_props\n", "pos_xor = new_pos ^ old_pos\n", @@ -865,26 +890,26 @@ " user.renew = True\n", " user.cipher.combined_position_key = user.cipher.combined_position_key ^ pos_xor\n", " user.cipher.property_key = user.cipher.property_key ^ props_xor" - ], - "outputs": [], - "execution_count": 24 + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "### New User Keys\n", "After a user's first successful login, the renew flag is checked. If it's true, the user's cipher is replaced with a new cipher." ] }, { + "cell_type": "code", + "execution_count": 25, "metadata": { "ExecuteTime": { "end_time": "2025-03-27T19:17:59.752960Z", "start_time": "2025-03-27T19:17:59.508826Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "if user.renew:\n", " user.cipher = UserCipher.create(\n", @@ -894,30 +919,28 @@ " )\n", " user.enciphered_passcode = user.cipher.encipher_nkode(presumed_property_indices, customer.cipher)\n", " user.renew = False" - ], - "outputs": [], - "execution_count": 25 + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.10.14" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 } diff --git a/notebooks/Enrollment_Login_Renewal_Simplified.ipynb b/notebooks/Enrollment_Login_Renewal_Simplified.ipynb index 0394de6..23a65cc 100644 --- a/notebooks/Enrollment_Login_Renewal_Simplified.ipynb +++ b/notebooks/Enrollment_Login_Renewal_Simplified.ipynb @@ -2,6 +2,18 @@ "cells": [ { "cell_type": "code", + "execution_count": 30, + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-28T15:06:18.878127Z", + "start_time": "2025-03-28T15:06:18.874618Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], "source": [ "import sys\n", "import os\n", @@ -15,20 +27,11 @@ "\n", "def random_username() -> str:\n", " return \"test_username\" + \"\".join([choice(ascii_lowercase) for _ in range(6)])\n" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2025-03-28T15:06:18.878127Z", - "start_time": "2025-03-28T15:06:18.874618Z" - } - }, - "outputs": [], - "execution_count": 30 + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "## Initialize nKode API and Create Customer\n", "#### nKode Customer\n", @@ -39,6 +42,18 @@ }, { "cell_type": "code", + "execution_count": 31, + "metadata": { + "ExecuteTime": { + "end_time": "2025-03-28T15:06:18.896461Z", + "start_time": "2025-03-28T15:06:18.891125Z" + }, + "collapsed": false, + "jupyter": { + "outputs_hidden": false + } + }, + "outputs": [], "source": [ "api = NKodeAPI()\n", "policy = NKodePolicy(\n", @@ -53,20 +68,11 @@ ")\n", "customer_id = api.create_new_customer(keypad_size, policy)\n", "customer = api.get_customer(customer_id)" - ], - "metadata": { - "collapsed": false, - "ExecuteTime": { - "end_time": "2025-03-28T15:06:18.896461Z", - "start_time": "2025-03-28T15:06:18.891125Z" - } - }, - "outputs": [], - "execution_count": 31 + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "## nKode Enrollment\n", "Users enroll in three steps:\n", @@ -79,72 +85,72 @@ ] }, { + "cell_type": "code", + "execution_count": 32, "metadata": { "ExecuteTime": { "end_time": "2025-03-28T15:06:18.914254Z", "start_time": "2025-03-28T15:06:18.911798Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "username = random_username()\n", "signup_session_id, set_keypad = api.generate_signup_keypad(customer_id, username)" - ], - "outputs": [], - "execution_count": 32 + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "### Set nKode\n", "The client receives `user_icons`, `set_signup_keypad`\n" ] }, { + "cell_type": "code", + "execution_count": 33, "metadata": { "ExecuteTime": { "end_time": "2025-03-28T15:06:18.931791Z", "start_time": "2025-03-28T15:06:18.929028Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "passcode_len = 4\n", "passcode_property_indices = np.random.choice(set_keypad.reshape(-1), size=passcode_len, replace=False).tolist()\n", "selected_keys_set = select_keys_with_passcode_values(passcode_property_indices, set_keypad, keypad_size.numb_of_keys)" - ], - "outputs": [], - "execution_count": 33 + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "### Confirm nKode\n", "The user enter then submits their key entry. The server returns the confirm_keypad, another index array and dispersion of the set_keypad." ] }, { + "cell_type": "code", + "execution_count": 34, "metadata": { "ExecuteTime": { "end_time": "2025-03-28T15:06:19.247638Z", "start_time": "2025-03-28T15:06:18.938601Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "confirm_keypad = api.set_nkode(customer_id, selected_keys_set, signup_session_id)\n", "selected_keys_confirm = select_keys_with_passcode_values(passcode_property_indices, confirm_keypad, keypad_size.numb_of_keys)\n", "success = api.confirm_nkode(customer_id, selected_keys_confirm, signup_session_id)\n", "assert success" - ], - "outputs": [], - "execution_count": 34 + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "### User Login\n", "1. Get login keypad\n", @@ -152,25 +158,25 @@ ] }, { + "cell_type": "code", + "execution_count": 35, "metadata": { "ExecuteTime": { "end_time": "2025-03-28T15:06:19.559753Z", "start_time": "2025-03-28T15:06:19.254675Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "login_keypad = api.get_login_keypad(username, customer_id)\n", "selected_keys_login = select_keys_with_passcode_values(passcode_property_indices, login_keypad, keypad_size.props_per_key)\n", "success = api.login(customer_id, username, selected_keys_login)\n", "assert success" - ], - "outputs": [], - "execution_count": 35 + ] }, { - "metadata": {}, "cell_type": "markdown", + "metadata": {}, "source": [ "## Renew Properties\n", "Replace server-side ciphers keys and nkode hash with new values.\n", @@ -180,60 +186,60 @@ ] }, { + "cell_type": "code", + "execution_count": 36, "metadata": { "ExecuteTime": { "end_time": "2025-03-28T15:06:20.181548Z", "start_time": "2025-03-28T15:06:19.568067Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "api.renew_keys(customer_id) # Steps 1 and 2\n", "login_keypad = api.get_login_keypad(username, customer_id)\n", "selected_keys_login = select_keys_with_passcode_values(passcode_property_indices, login_keypad, keypad_size.props_per_key)\n", "success = api.login(customer_id, username, selected_keys_login) # Step 3\n", "assert success" - ], - "outputs": [], - "execution_count": 36 + ] }, { + "cell_type": "code", + "execution_count": 37, "metadata": { "ExecuteTime": { "end_time": "2025-03-28T15:06:20.500050Z", "start_time": "2025-03-28T15:06:20.194912Z" } }, - "cell_type": "code", + "outputs": [], "source": [ "login_keypad = api.get_login_keypad(username, customer_id)\n", "selected_keys_login = select_keys_with_passcode_values(passcode_property_indices, login_keypad, keypad_size.props_per_key)\n", "success = api.login(customer_id, username, selected_keys_login)\n", "assert success" - ], - "outputs": [], - "execution_count": 37 + ] } ], "metadata": { "kernelspec": { - "display_name": "Python 3", + "display_name": "Python 3 (ipykernel)", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", - "version": 2 + "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", - "pygments_lexer": "ipython2", - "version": "2.7.6" + "pygments_lexer": "ipython3", + "version": "3.10.14" } }, "nbformat": 4, - "nbformat_minor": 0 + "nbformat_minor": 4 } diff --git a/src/nkode_cipher_v2/__init__.py b/src/nkode_cipher_v2/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/nkode_cipher_v2/nkode_cipher.py b/src/nkode_cipher_v2/nkode_cipher.py new file mode 100644 index 0000000..4c5e66d --- /dev/null +++ b/src/nkode_cipher_v2/nkode_cipher.py @@ -0,0 +1,128 @@ +import base64 +import hashlib +from dataclasses import dataclass +import bcrypt +import numpy as np +from src.models import EncipheredNKode, KeypadSize + +@dataclass +class NKodeCipher: + property_key: np.ndarray + position_key: np.ndarray + pass_key: np.ndarray + mask_key: np.ndarray + keypad_size: KeypadSize + max_nkode_len: int + + @classmethod + def create(cls, keypad_size: KeypadSize, max_nkode_len: int) -> 'NKodeCipher': + return NKodeCipher( + property_key=np.random.choice(2 ** 16, size=keypad_size.total_props, replace=False), + pass_key=np.random.choice(2 ** 16, size=max_nkode_len, replace=False), + mask_key=np.random.choice(2**16, size=max_nkode_len, replace=False), + position_key= np.random.choice(2**16,size=keypad_size.props_per_key, replace=False), + max_nkode_len=max_nkode_len, + keypad_size=keypad_size, + ) + + def get_props_position_vals(self, props: np.ndarray | list[int]) -> np.ndarray: + if not all([prop in self.property_key for prop in props]): + raise ValueError("Property values must be within valid range") + pos_vals = [self._get_prop_position_val(prop) for prop in props] + return np.array(pos_vals) + + def _get_prop_position_val(self, prop: int) -> int: + assert prop in self.property_key + prop_idx = np.where(self.property_key == prop)[0][0] + pos_idx = prop_idx % self.keypad_size.props_per_key + return int(self.position_key[pos_idx]) + + def pad_user_mask(self, user_mask: np.ndarray, pos_vals: np.ndarray) -> np.ndarray: + # TODO: replace with new method + if len(user_mask) >= self.max_nkode_len: + raise ValueError("User encoded_mask is too long") + padding_size = self.max_nkode_len - len(user_mask) + padding = np.random.choice(pos_vals, size=padding_size, replace=True).astype(np.uint16) + return np.concatenate([user_mask, padding]) + + @staticmethod + def encode_base64_str(data: np.ndarray) -> str: + return base64.b64encode( b"".join([int(num).to_bytes(2, byteorder='big') for num in data])).decode("utf-8") + + @staticmethod + def decode_base64_str(data: str) -> np.ndarray: + byte_data = base64.b64decode(data) + int_list = [] + for i in range(0, len(byte_data), 2): + int_val = int.from_bytes(byte_data[i:i + 2], byteorder='big') + int_list.append(int_val) + return np.array(int_list, dtype=np.uint16) + + def encipher_nkode( + self, + passcode_prop_idx: list[int], + ) -> EncipheredNKode: + mask = self.encipher_mask(passcode_prop_idx) + code = self.hash_nkode(passcode_prop_idx) + return EncipheredNKode( + code=code, + mask=mask + ) + + def hash_nkode( + self, + passcode_prop_idx: list[int], + ) -> str: + salt = bcrypt.gensalt(rounds=12) + passcode_bytes = self.prehash_passcode(passcode_prop_idx) + passcode_digest = base64.b64encode(hashlib.sha256(passcode_bytes).digest()) + hashed_data = bcrypt.hashpw(passcode_digest, salt) + return hashed_data.decode("utf-8") + + def compare_nkode( + self, + passcode_prop_idx: list[int], + hashed_passcode: str + ) -> bool: + passcode_bytes = self.prehash_passcode(passcode_prop_idx) + passcode_digest = base64.b64encode(hashlib.sha256(passcode_bytes).digest()) + return bcrypt.checkpw(passcode_digest, hashed_passcode.encode('utf-8')) + + def prehash_passcode( + self, + passcode_prop_idx: list[int], + ) -> bytes: + passcode_len = len(passcode_prop_idx) + passcode_cipher = self.pass_key.copy() + passcode_cipher[:passcode_len] = ( + passcode_cipher[:passcode_len] ^ + self.property_key[passcode_prop_idx] + ) + return passcode_cipher.astype(np.uint16).tobytes() + + def get_passcode_position_indices_padded(self, passcode_indices: list[int]) -> list[int]: + pos_indices = [idx % self.keypad_size.props_per_key for idx in passcode_indices] + pad_len = self.max_nkode_len - len(passcode_indices) + pad = np.random.choice(self.keypad_size.props_per_key, pad_len, replace=True) + return pos_indices + pad.tolist() + + def encipher_mask( + self, + passcode_prop_idx: list[int], + ) -> str: + pos_idxs = self.get_passcode_position_indices_padded(passcode_prop_idx) + ordered_pos_key = self.position_key[pos_idxs] + mask = ordered_pos_key ^ self.mask_key + encoded_mask = self.encode_base64_str(mask) + return encoded_mask + + def decipher_mask(self, encoded_mask: str, passcode_len: int) -> list[int]: + mask = self.decode_base64_str(encoded_mask) + # user_pos_key ordered by the user's nkode and padded to be length max_nkode_len + ordered_user_pos_key = mask ^ self.mask_key + passcode_position = [] + for position_val in ordered_user_pos_key[:passcode_len]: + position_idx = np.where(self.position_key == position_val)[0][0] + passcode_position.append(int(self.position_key[position_idx])) + return passcode_position + diff --git a/test/test_nkode_cipher_keys.py b/test/test_nkode_cipher_keys.py new file mode 100644 index 0000000..a41074b --- /dev/null +++ b/test/test_nkode_cipher_keys.py @@ -0,0 +1,37 @@ +import numpy as np +import pytest +from src.models import KeypadSize +from src.nkode_cipher_v2.nkode_cipher import NKodeCipher + + +@pytest.mark.parametrize( + "passcode_len", + [ + 6 + ] +) +def test_encode_decode_base64(passcode_len): + data = np.random.choice(2**16, passcode_len, replace=False) + encoded = NKodeCipher.encode_base64_str(data) + decoded = NKodeCipher.decode_base64_str(encoded) + assert (len(data) == len(decoded)) + assert (all(data[idx] == decoded[idx] for idx in range(passcode_len))) + + +@pytest.mark.parametrize( + "keypad_size,max_nkode_len", + [ + (KeypadSize(numb_of_keys=10, props_per_key=11), 10), + (KeypadSize(numb_of_keys=9, props_per_key=11), 10), + (KeypadSize(numb_of_keys=8, props_per_key=11), 12), + ]) +def test_decode_mask(keypad_size, max_nkode_len): + passcode_entry = np.random.choice(keypad_size.total_props, 4, replace=False) + user_keys = NKodeCipher.create(keypad_size, max_nkode_len) + passcode_values = [user_keys.property_key[idx] for idx in passcode_entry] + enciphered = user_keys.encipher_nkode(passcode_entry) + orig_passcode_pos_vals = user_keys.get_props_position_vals(passcode_values) + passcode_pos_vals = user_keys.decipher_mask(enciphered.mask, len(passcode_entry)) + assert (len(passcode_pos_vals) == len(orig_passcode_pos_vals)) + assert (all(orig_passcode_pos_vals[idx] == passcode_pos_vals[idx] for idx in range(len(passcode_pos_vals)))) + assert(user_keys.compare_nkode(passcode_entry, enciphered.code))