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))