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) + + +def note(text): + 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) + + +def note(text): + 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) + + +def note(text): + display(HTML(f"

{text}

"), append=True)