Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions examples/pynacl/README.md
Original file line number Diff line number Diff line change
@@ -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.
5 changes: 5 additions & 0 deletions examples/pynacl/order.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[
"secret_box_basics",
"public_key_box",
"signing_and_hashing"
]
55 changes: 55 additions & 0 deletions examples/pynacl/public_key_box/code.py
Original file line number Diff line number Diff line change
@@ -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 <em>my private key</em> and "
"<em>their public key</em> 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"<code>{bob_public.encode(HexEncoder).decode()}</code>")
note(f"Alice's public key (hex): "
f"<code>{alice_public.encode(HexEncoder).decode()}</code>")

# 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: <strong>{plaintext.decode()}</strong>")

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: <strong>{unsealed.decode()}</strong>")
1 change: 1 addition & 0 deletions examples/pynacl/public_key_box/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["pynacl"]
20 changes: 20 additions & 0 deletions examples/pynacl/public_key_box/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)

60 changes: 60 additions & 0 deletions examples/pynacl/secret_box_basics/code.py
Original file line number Diff line number Diff line change
@@ -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): <code>{key.hex()}</code>")
note(f"Plaintext length: {len(message)} bytes")
note(f"Ciphertext length: {len(encrypted.ciphertext)} bytes "
f"(plaintext + 16-byte authenticator)")
note(f"Nonce (hex): <code>{encrypted.nonce.hex()}</code>")

# 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: <strong>{plaintext.decode()}</strong>")

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 <code>CryptoError</code>: {exc}")
1 change: 1 addition & 0 deletions examples/pynacl/secret_box_basics/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["pynacl"]
40 changes: 40 additions & 0 deletions examples/pynacl/secret_box_basics/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)
85 changes: 85 additions & 0 deletions examples/pynacl/signing_and_hashing/code.py
Original file line number Diff line number Diff line change
@@ -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"<code>{verify_key.encode(HexEncoder).decode()}</code>")

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: <strong>{verified_message.decode()}</strong>")

# 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: <code>BadSignatureError</code>: {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) = <code>{digest.decode()}</code>")

# ---------------------------------------------------------------------
# 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: <code>{verifier.decode()}</code>")

# Successful verification returns True.
ok = nacl.pwhash.verify(verifier, password)
note(f"Correct password verifies: <strong>{ok}</strong>")

# A wrong password raises InvalidkeyError.
try:
nacl.pwhash.verify(verifier, b"hunter2")
except InvalidkeyError as exc:
note(f"Wrong password rejected: <code>InvalidkeyError</code>: {exc}")
1 change: 1 addition & 0 deletions examples/pynacl/signing_and_hashing/config.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
packages = ["pynacl"]
19 changes: 19 additions & 0 deletions examples/pynacl/signing_and_hashing/setup.py
Original file line number Diff line number Diff line change
@@ -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"<h{level}>{text}</h{level}>"), append=True)


def note(text):
display(HTML(f"<p>{text}</p>"), append=True)