simonw / datasette-secret-santa

Run secret santa gift circles using Datasette
Apache License 2.0
3 stars 1 forks source link

Build a secret santa system #1

Closed simonw closed 1 year ago

simonw commented 1 year ago

For family secret santa for Christmas.

simonw commented 1 year ago

You can add people to a secret santa group (unless it has been executed)

People have to claim their secret codes by clicking a link (and a button to generate the code) - they then have to note the code down.

It generates and stores a private key for them, but one that can only be recovered using the secret code they noted down.

When the last participant has grabbed their code, it randomly assigned gift pairs to people.

Then they can enter their code to find out who they should give to.

simonw commented 1 year ago

Prompted ChatGPT:

Simplest possible Python code for creating a public/private key pair and then using the public key to encrypt a message, then the private key to decrypt it

It gave me back:

from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.primitives import serialization, hashes
from cryptography.hazmat.primitives.asymmetric import padding

# Generate a public/private key pair
private_key = rsa.generate_private_key(
    public_exponent=65537,
    key_size=2048
)
public_key = private_key.public_key()

# Serialize the keys for storage or transmission
private_key_bytes = private_key.private_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PrivateFormat.PKCS8,
    encryption_algorithm=serialization.NoEncryption()
)
public_key_bytes = public_key.public_bytes(
    encoding=serialization.Encoding.PEM,
    format=serialization.PublicFormat.SubjectPublicKeyInfo
)

# Encrypt the message using the public key
message = "Hello World!"
encrypted_message = public_key.encrypt(
    message,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

# Decrypt the message using the private key
decrypted_message = private_key.decrypt(
    encrypted_message,
    padding.OAEP(
        mgf=padding.MGF1(algorithm=hashes.SHA256()),
        algorithm=hashes.SHA256(),
        label=None
    )
)

Ran this and got:

TypeError: initializer for ctype 'unsigned char *' must be a bytes or list or tuple, not str

Fix was to use `message = b"Hello World!" instead.

simonw commented 1 year ago

Simple Python code to use symetric encryption to encode a long binary string using the secret key "badger-scottish-womble"

import os
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend

# Encrypt the message using the secret key
# This would ensure that only someone who knows the secret key can decrypt the message
def encrypt_message(secret_key, message):
    iv = os.urandom(16)  # generate random initialization vector
    cipher = Cipher(algorithms.AES(secret_key), modes.CBC(iv), backend=default_backend())
    encryptor = cipher.encryptor()
    ct = encryptor.update(message) + encryptor.finalize()
    return (iv, ct)

# Encrypt the binary string using the secret key "badger-scottish-womble"
secret_key = "badger-scottish-womble"
binary_string = b"\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F"
iv, ct = encrypt_message(secret_key, binary_string)
print(iv, ct)

To decode:

# Decrypt the message using the secret key
# This would allow the recipient to read the message
def decrypt_message(secret_key, iv, ct):
    cipher = Cipher(algorithms.AES(secret_key), modes.CBC(iv), backend=default_backend())
    decryptor = cipher.decryptor()
    message = decryptor.update(ct) + decryptor.finalize()
    return message

# Decrypt the message using the secret key "badger-scottish-womble"
secret_key = "badger-scottish-womble"
iv, ct = (iv, ct)  # iv and ct are the encrypted message returned by the encrypt_message function
binary_string = decrypt_message(secret_key, iv, ct)
print(binary_string)
simonw commented 1 year ago

After a few rounds through ChatGPT to figure out padding stuff I got to this:

import os
import hashlib
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives.padding import PKCS7

# Encrypt the message using the secret key
# This would ensure that only someone who knows the secret key can decrypt the message
def encrypt_message(secret_key, message):
    secret_key = hashlib.sha256(secret_key).digest()
    padder = PKCS7(128).padder()
    padded_data = padder.update(message) + padder.finalize()
    iv = os.urandom(16)  # generate random initialization vector
    cipher = Cipher(algorithms.AES(secret_key), modes.CBC(iv), backend=default_backend())
    encryptor = cipher.encryptor()
    ct = encryptor.update(padded_data) + encryptor.finalize()
    return (iv, ct)

# Encrypt the binary string using the secret key "badger-scottish-womble"
secret_key = b"badger-scottish-womble"
binary_string = b"this will be encrypted"
iv, ct = encrypt_message(secret_key, binary_string)
print(iv, ct)

# Decrypt the message using the secret key
# This would allow the recipient to read the message
def decrypt_message(secret_key, iv, ct):
    secret_key = hashlib.sha256(secret_key).digest()
    cipher = Cipher(algorithms.AES(secret_key), modes.CBC(iv), backend=default_backend())
    decryptor = cipher.decryptor()
    padded_data = decryptor.update(ct) + decryptor.finalize()
    unpadder = PKCS7(128).unpadder()
    return unpadder.update(padded_data) + unpadder.finalize()

# Decrypt the message using the secret key "badger-scottish-womble"
secret_key = b"badger-scottish-womble"
iv, ct = (iv, ct)  # iv and ct are the encrypted message returned by the encrypt_message function
binary_string = decrypt_message(secret_key, iv, ct)
print(binary_string)
simonw commented 1 year ago

My ChatGPT prompts to get there were:

ValueError: Invalid key size (176) for AES.

ValueError: The length of the provided data is not a multiple of the block length.

Show me the decrypt code using padding again:

That last one was needed because ChatGPT ran out of space and truncated the last code example.

simonw commented 1 year ago

I now have the code I need to do both symmetric and asymmetric encryption!

So when a user requests their secret password:

The code that randomly assigns secret santa matches then encrypts the message to each user using their public key

When a user comes to see who they got:

simonw commented 1 year ago

For the random words, found this: https://www.eff.org/deeplinks/2016/07/new-wordlists-random-passphrases

I like https://www.eff.org/files/2016/07/18/eff_large_wordlist.txt

simonw commented 1 year ago

https://www.eff.org/files/2016/09/08/eff_short_wordlist_1.txt has words that are easier to spell.

simonw commented 1 year ago

Problem: a server administrator could brute-force the system to find the passwords.

Solution: run them through a few hundred thousand rounds of bcrypt before using them to encrypt/decrypt the secret used to AES encrypt the private key. [I ended up doing something smarter than this, see #6]

simonw commented 1 year ago

I said to ChatGPT:

Use the Python cryptography library to securely hash a password with many iterations to protect against brute force attacks. Show me several options using different algorithms

First option it produced looked good to me:

from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC
from cryptography.hazmat.primitives import hashes

# password to be hashed
password = b'password123'

# generate random salt
salt = os.urandom(16)

# number of iterations (more iterations makes brute force attacks slower)
iterations = 100000

# create PBKDF2HMAC instance
kdf = PBKDF2HMAC(
    algorithm=hashes.SHA256(),
    length=32,
    salt=salt,
    iterations=iterations
)

# generate the hashed password
hashed_password = kdf.derive(password)
simonw commented 1 year ago

This code is interesting:

    return serialization.load_pem_private_key(
        private_key_raw,
        password=None,
        backend=default_backend()
    )

The password=None there suggests that maybe I don't need to include my own encryption scheme for private keys.

New issue:

simonw commented 1 year ago

This is happening in other issues now.

simonw commented 1 year ago

https://simonwillison.net/2022/Dec/11/over-engineering-secret-santa/