diff --git a/examples/pynacl/README.md b/examples/pynacl/README.md
new file mode 100644
index 0000000..1f2e5ab
--- /dev/null
+++ b/examples/pynacl/README.md
@@ -0,0 +1,18 @@
+# pynacl Examples
+
+Each sub-directory contains a self-contained example. The order in
+which the examples are to appear is specified in `order.json` (an
+array of directory names in the expected order).
+
+In each example directory you'll find:
+
+* `config.toml` - must conform to the specification outlined here:
+ https://docs.pyscript.net/latest/user-guide/configuration/ This is
+ parsed and ultimately turned into a JSON representation as part of
+ the package's API object.
+* `setup.py` - Python code for contextual and environmental setup,
+ NOT SEEN BY THE END USER, but is run before the `code.py` code is
+ evaluated. Allows us to create useful (IPython) shims, avoid
+ repeating boilerplate and whatnot.
+* `code.py` - the actual code added to the editor which forms the
+ practical example of using the package.
diff --git a/examples/pynacl/order.json b/examples/pynacl/order.json
new file mode 100644
index 0000000..eb79ffb
--- /dev/null
+++ b/examples/pynacl/order.json
@@ -0,0 +1,5 @@
+[
+ "secret_box_basics",
+ "public_key_box",
+ "signing_and_hashing"
+]
diff --git a/examples/pynacl/public_key_box/code.py b/examples/pynacl/public_key_box/code.py
new file mode 100644
index 0000000..a868863
--- /dev/null
+++ b/examples/pynacl/public_key_box/code.py
@@ -0,0 +1,55 @@
+# ---------------------------------------------------------------------
+# Alice and Bob exchange messages using Curve25519 key pairs.
+# ---------------------------------------------------------------------
+from nacl.public import PrivateKey, Box, SealedBox
+from nacl.encoding import HexEncoder
+
+
+heading("Alice and Bob exchange keys")
+note(
+ "Each party generates a private key and shares the matching "
+ "public key. A Box built from my private key and "
+ "their public key can encrypt to them and decrypt "
+ "from them."
+)
+
+# Bob's long-term key pair.
+bob_secret = PrivateKey.generate()
+bob_public = bob_secret.public_key
+
+# Alice's long-term key pair.
+alice_secret = PrivateKey.generate()
+alice_public = alice_secret.public_key
+
+note(f"Bob's public key (hex): "
+ f"{bob_public.encode(HexEncoder).decode()}")
+note(f"Alice's public key (hex): "
+ f"{alice_public.encode(HexEncoder).decode()}")
+
+# Bob prepares a Box to send messages to Alice.
+bob_to_alice = Box(bob_secret, alice_public)
+encrypted = bob_to_alice.encrypt(b"Meet me by the old oak at midnight.")
+
+note(f"Ciphertext length: {len(encrypted.ciphertext)} bytes")
+
+# Alice opens the matching Box to read it.
+alice_from_bob = Box(alice_secret, bob_public)
+plaintext = alice_from_bob.decrypt(encrypted)
+note(f"Alice reads: {plaintext.decode()}")
+
+heading("Anonymous messages with SealedBox")
+note(
+ "A SealedBox lets anyone send a message to a recipient using "
+ "only the recipient's public key. Each message uses a fresh "
+ "ephemeral sender key that is destroyed after encryption, so "
+ "even the sender cannot decrypt it later."
+)
+
+# Anyone holding Bob's public key can seal a message to him.
+sealed = SealedBox(bob_public).encrypt(b"An anonymous tip for Bob.")
+note(f"Sealed ciphertext length: {len(sealed)} bytes "
+ f"(includes a 32-byte ephemeral public key)")
+
+# Only Bob, with his private key, can unseal it.
+unsealed = SealedBox(bob_secret).decrypt(sealed)
+note(f"Bob unseals: {unsealed.decode()}")
diff --git a/examples/pynacl/public_key_box/config.toml b/examples/pynacl/public_key_box/config.toml
new file mode 100644
index 0000000..c481e4d
--- /dev/null
+++ b/examples/pynacl/public_key_box/config.toml
@@ -0,0 +1 @@
+packages = ["pynacl"]
diff --git a/examples/pynacl/public_key_box/setup.py b/examples/pynacl/public_key_box/setup.py
new file mode 100644
index 0000000..3b9b156
--- /dev/null
+++ b/examples/pynacl/public_key_box/setup.py
@@ -0,0 +1,20 @@
+"""Lighter setup for example 2: same names, no IPython shim."""
+import js
+from pyscript import window, HTML, display as _display
+
+js.alert = window.alert
+
+
+def display(*args, **kwargs):
+ return _display(
+ *args, **kwargs, target=__pyscript_display_target__,
+ )
+
+
+def heading(text, level=2):
+ display(HTML(f"
{text}
"), append=True) + diff --git a/examples/pynacl/secret_box_basics/code.py b/examples/pynacl/secret_box_basics/code.py new file mode 100644 index 0000000..7c3d677 --- /dev/null +++ b/examples/pynacl/secret_box_basics/code.py @@ -0,0 +1,60 @@ +""" +A first taste of PyNaCl: symmetric (secret-key) encryption. + +Two parties who already share a secret key can use SecretBox to +exchange confidential, authenticated messages. The ciphertext is +sealed with a 16-byte authenticator: any tampering causes +decryption to fail loudly. + +Docs: https://pynacl.readthedocs.io/en/latest/secret/ +""" +from IPython.core.display import display, HTML + +# Package imports for this example. +import nacl.utils +from nacl.secret import SecretBox + + +heading("A diary entry, locked with a shared key") +note( + "We generate a random 32-byte key, encrypt a short message, " + "and then decrypt it again. The same key is used for both " + "operations, so it must be kept secret." +) + +# A SecretBox key is exactly 32 random bytes. +key = nacl.utils.random(SecretBox.KEY_SIZE) +box = SecretBox(key) + +message = b"Dear diary: today I learned about libsodium." + +# When the nonce is omitted, PyNaCl picks a fresh random one. +# A nonce must NEVER be reused with the same key. +encrypted = box.encrypt(message) + +note(f"Key (hex):{key.hex()}")
+note(f"Plaintext length: {len(message)} bytes")
+note(f"Ciphertext length: {len(encrypted.ciphertext)} bytes "
+ f"(plaintext + 16-byte authenticator)")
+note(f"Nonce (hex): {encrypted.nonce.hex()}")
+
+# The EncryptedMessage carries both the nonce and the ciphertext,
+# so we can pass it straight back into decrypt().
+plaintext = box.decrypt(encrypted)
+note(f"Decrypted message: {plaintext.decode()}")
+
+heading("Tampering is detected")
+note(
+ "If anyone flips a single bit of the ciphertext, decryption "
+ "raises a CryptoError instead of returning garbage."
+)
+
+from nacl.exceptions import CryptoError
+
+# Flip one byte of the ciphertext to simulate tampering.
+tampered = bytearray(encrypted)
+tampered[-1] ^= 0x01
+try:
+ box.decrypt(bytes(tampered))
+except CryptoError as exc:
+ note(f"Caught CryptoError: {exc}")
diff --git a/examples/pynacl/secret_box_basics/config.toml b/examples/pynacl/secret_box_basics/config.toml
new file mode 100644
index 0000000..c481e4d
--- /dev/null
+++ b/examples/pynacl/secret_box_basics/config.toml
@@ -0,0 +1 @@
+packages = ["pynacl"]
diff --git a/examples/pynacl/secret_box_basics/setup.py b/examples/pynacl/secret_box_basics/setup.py
new file mode 100644
index 0000000..84faac4
--- /dev/null
+++ b/examples/pynacl/secret_box_basics/setup.py
@@ -0,0 +1,40 @@
+"""
+Shim IPython's display API onto PyScript so example code written in a
+Jupyter/IPython idiom runs unmodified in the browser.
+"""
+
+import sys
+import types
+import js
+from pyscript import window, HTML, display as _display
+
+js.alert = window.alert
+
+
+def display(*args, **kwargs):
+ return _display(
+ *args, **kwargs, target=__pyscript_display_target__,
+ )
+
+
+ipython = types.ModuleType("IPython")
+core = types.ModuleType("IPython.core")
+core_display = types.ModuleType("IPython.core.display")
+core_display.display = display
+core_display.HTML = HTML
+ipython.core = core
+core.display = core_display
+ipython.get_ipython = lambda: None
+ipython.display = core_display
+sys.modules["IPython"] = ipython
+sys.modules["IPython.core"] = core
+sys.modules["IPython.core.display"] = core_display
+sys.modules["IPython.display"] = core_display
+
+
+def heading(text, level=2):
+ display(HTML(f"{text}
"), append=True) diff --git a/examples/pynacl/signing_and_hashing/code.py b/examples/pynacl/signing_and_hashing/code.py new file mode 100644 index 0000000..8c0471d --- /dev/null +++ b/examples/pynacl/signing_and_hashing/code.py @@ -0,0 +1,85 @@ +# --------------------------------------------------------------------- +# Ed25519 digital signatures: prove who wrote a message. +# --------------------------------------------------------------------- + +import nacl.hash +import nacl.pwhash +from nacl.signing import SigningKey +from nacl.encoding import HexEncoder +from nacl.exceptions import BadSignatureError, InvalidkeyError + + +heading("Signing a release announcement") +note( + "A SigningKey produces signatures; the matching VerifyKey " + "checks them. Anyone with the verify key can confirm the " + "message came from the signer and was not modified." +) + +signing_key = SigningKey.generate() +verify_key = signing_key.verify_key + +note(f"Verify key (hex): " + f"{verify_key.encode(HexEncoder).decode()}")
+
+announcement = b"Version 2.0 is out. Download from the official site."
+signed = signing_key.sign(announcement)
+
+note(f"Signed bundle length: {len(signed)} bytes "
+ f"(64-byte signature + {len(announcement)}-byte message)")
+
+# Verification returns the original message, or raises on tampering.
+verified_message = verify_key.verify(signed)
+note(f"Verified message: {verified_message.decode()}")
+
+# Demonstrate detection of a forgery.
+forged = signed[:-1] + bytes([signed[-1] ^ 0x01])
+try:
+ verify_key.verify(forged)
+except BadSignatureError as exc:
+ note(f"Forgery rejected: BadSignatureError: {exc}")
+
+# ---------------------------------------------------------------------
+# Hashing a message with BLAKE2b.
+# ---------------------------------------------------------------------
+
+heading("Fingerprinting data with BLAKE2b")
+note(
+ "nacl.hash.blake2b produces a fast, keyed cryptographic hash. "
+ "Useful for deduplication, integrity checks, or building MACs."
+)
+
+digest = nacl.hash.blake2b(announcement, encoder=HexEncoder)
+note(f"BLAKE2b(announcement) = {digest.decode()}")
+
+# ---------------------------------------------------------------------
+# Password hashing with Argon2id.
+# ---------------------------------------------------------------------
+
+heading("Storing passwords safely with Argon2id")
+note(
+ "Never store raw passwords. nacl.pwhash.argon2id.str() returns "
+ "a self-contained verifier string with a random salt and tunable "
+ "cost parameters baked in. Use INTERACTIVE limits for login "
+ "flows; SENSITIVE limits for high-value secrets."
+)
+
+password = b"correct horse battery staple"
+
+# Use INTERACTIVE limits so the demo runs quickly in the browser.
+verifier = nacl.pwhash.argon2id.str(
+ password,
+ opslimit=nacl.pwhash.argon2id.OPSLIMIT_INTERACTIVE,
+ memlimit=nacl.pwhash.argon2id.MEMLIMIT_INTERACTIVE,
+)
+note(f"Stored verifier: {verifier.decode()}")
+
+# Successful verification returns True.
+ok = nacl.pwhash.verify(verifier, password)
+note(f"Correct password verifies: {ok}")
+
+# A wrong password raises InvalidkeyError.
+try:
+ nacl.pwhash.verify(verifier, b"hunter2")
+except InvalidkeyError as exc:
+ note(f"Wrong password rejected: InvalidkeyError: {exc}")
diff --git a/examples/pynacl/signing_and_hashing/config.toml b/examples/pynacl/signing_and_hashing/config.toml
new file mode 100644
index 0000000..c481e4d
--- /dev/null
+++ b/examples/pynacl/signing_and_hashing/config.toml
@@ -0,0 +1 @@
+packages = ["pynacl"]
diff --git a/examples/pynacl/signing_and_hashing/setup.py b/examples/pynacl/signing_and_hashing/setup.py
new file mode 100644
index 0000000..5651908
--- /dev/null
+++ b/examples/pynacl/signing_and_hashing/setup.py
@@ -0,0 +1,19 @@
+"""Lighter setup for example 3: same names, no IPython shim."""
+import js
+from pyscript import window, HTML, display as _display
+
+js.alert = window.alert
+
+
+def display(*args, **kwargs):
+ return _display(
+ *args, **kwargs, target=__pyscript_display_target__,
+ )
+
+
+def heading(text, level=2):
+ display(HTML(f"{text}
"), append=True)