今年我们正在做一个家庭秘密圣诞老人,我们需要一种方法来随机分配人给彼此,而没有人知道谁被分配给了谁。
我提议写一些软件! (也许“坚持”更准确)
多年来,我一直想找个借口写一些涉及 Python密码库的有趣内容。问题是我太负责任/太懦弱了,无法忽略许多警告,如果您确切地知道自己在做什么,则只能使用该图书馆的“危险材料”区域。
秘密圣诞老人是完美的低风险项目,可以忽略这些警告并玩一些有趣的东西。
我的要求
我有六个参与者。每个参与者都需要知道他们要为谁买礼物——没有办法找出任何其他礼物配对。
作为系统的管理员,我也一定无法弄清楚配对。
我不想使用电子邮件或登录名或类似的东西 – 我只想能够在家庭 WhatsApp 组中共享一个链接,让每个人都使用相同的界面来进行配对。
这个怎么运作
这是我想出的方案:
- 每个参与者都会得到一个为他们生成的密码。当他们点击一个按钮时,这是按需发生的——有一个荣誉系统,不会点击别人的按钮(很容易检测到,因为每个按钮只能点击一次)。如果有人确实点击了其他人的按钮,我们可以重置整个系统并重新开始。
- 他们的密码是为他们生成的 – 这是三个随机单词,例如“squirrel copper sailboat”。我估计大多数人都会用手机截图记录下来。
- 在幕后,每个用户都有一个为他们生成的 RSA 公钥/私钥。私钥使用他们的新密码加密,然后都存储在数据库中。
- 一旦每个用户都生成并记录了他们的密码,我们就可以执行 Secret Santa 任务。这个简单的洗牌参与者,然后将每个人分配给列表中他们之后的人。然后它使用他们的公钥加密一条消息,告诉他们应该为谁买礼物。
- 这些加密的消息也存储在数据库中。
- 最后,每个用户都可以返回站点并输入他们的密码来解密和查看他们的消息。
这是一个动画 GIF 演示:
将其构建为 Datasette 插件
这是一个需要非常少量持久性的小型应用程序,因此我决定将其构建为几个 SQLite 数据库表之上的 Datasette 插件。
除了给我一个在我的主项目中尝试新事物的借口之外,这也应该能让它更容易部署。
大部分代码都在datasette_secret_santa/__init__.py文件中。我使用了许多不同的插件挂钩:
-
startup()
在服务器首次启动时创建它需要的数据库表(如果它们不存在) -
canned_queries()
添加用于创建新的秘密圣诞老人组的固定 SQL 查询,使我无需为此构建自定义 UI -
register_routes()
在 Datasette 中注册五个新的自定义页面 extra_template_vars()
使 Datasette 主页上的额外上下文变量可用,该变量使用自定义模板呈现
以下是路线:
@hookimpl def register_routes (): 返回[ ( r"^/secret-santa/(?P<slug>[^/]+)$" , secret_santa ), ( r"^/secret-santa/(?P<slug>[^/]+)/add$" , add_participant ), ( r"^/secret-santa/(?P<slug>[^/]+)/assign$" , assign_participants ), ( r"^/secret-santa/(?P<slug>[^/]+)/set-password/(?P<id>\d+)$" , set_password ) , ( r"^/secret-santa/(?P<slug>[^/]+)/reveal/(?P<id>\d+)$" , reveal ), ]
-
/secret-santa/{slug}
是秘密圣诞老人组的主页。它显示了参与者列表和用于添加新参与者的表单。 -
/secret-santa/{slug}/add
是添加新参与者的表单的端点。 -
/secret-santa/{slug}/set-password/{id}
是让用户生成和检索密码的页面。 -
/secret-santa/{slug}/reveal/{id}
是用户输入密码以显示他们的秘密圣诞老人任务的页面。 -
/secret-santa/{slug}/assign
是完成将参与者分配给彼此的工作的端点,并为每个参与者生成和保存加密消息。
密码学
早前的警告在这里成立:我不是密码学家。我只是在找乐子。你不应该模仿我在这里写的任何代码,而没有与知道他们在做什么的人彻底审查它。
(我还使用 ChatGPT 编写了它的初稿,如本期所述。信任由大型语言模型生成的加密代码是一个特别糟糕的主意!)
免责声明,这是我编写的用于生成和存储 RSA 密钥的代码:
async def generate_password_and_keys_for_user ( db , participant_id ): 密码= “” 。加入(随机。样本(单词, 3 )) 私钥= rsa 。 generate_private_key ( public_exponent = 65537 , key_size = 2048 ) 公钥=私钥。公钥() # 序列化存储的密钥 private_key_serialized = private_key 。 private_bytes ( 编码=序列化。编码。质子交换膜 格式=序列化。私有格式。 PKCS8 , 加密算法=序列化。最佳可用加密( 密码。编码( “utf-8” ) ), ).解码( “utf-8” ) public_key_serialized = public_key 。 public_bytes ( 编码=序列化。编码。质子交换膜 格式=序列化。公共格式。主题公钥信息, ).解码( “utf-8” ) 等待数据库。执行_写入( """ 更新 secret_santa_participants 放 password_issued_at = datetime('now'), public_key = :public_key, 私钥=:私钥 其中 id = :id """ , { “id” :参与者_id , “public_key” : public_key_serialized , “private_key” : private_key_serialized , }, ) 返回密码
如您所见,它使用PyCA 加密库中的rsa.generate_private_key()
来生成公钥和私钥。
generate_private_key() 文档推荐选项public_exponent=65537, key_size=2048
。
然后将它们序列化为可以存储在数据库中的 PEM 格式字符串。
私钥在使用为该用户随机生成的密码加密后被序列化。这会产生一个如下所示的字符串:
-----BEGIN ENCRYPTED PRIVATE KEY----- ... -----END ENCRYPTED PRIVATE KEY-----
我最初为此想出了自己的方案,涉及 AES 加密和从原始密码的哈希派生的密钥(我计划稍后通过bcrypt
运行几十万次)——当我意识到这一点时,我感到非常高兴已经有一种标准的方法可以做到这一点。
然后分配参与者并生成他们的加密消息的代码如下所示:
# 分配参与者 随机的。洗牌(参与者) 对于我,枚举参与者(参与者) : 分配=参与者[( i + 1 ) % len (参与者)] message = "You should buy a gift for {}" .格式(已分配[ “名称” ]) # 用他们的公钥加密消息 public_key =序列化。 load_pem_public_key ( 参与者[ “public_key” ]。编码( “utf-8” ),后端= default_backend () ) secret_message_encrypted = public_key 。加密( 留言。编码( “utf-8” ), 填充。环境规划署( mgf =填充。 MGF1 ( algorithm = hashes . SHA256 ()), 算法=哈希。 SHA256 (), 标签=无, ), ) 等待数据库。执行_写入( """ 更新 secret_santa_participants 设置 secret_message_encrypted = :secret_message_encrypted 其中 id = :id """ , { “id” :参与者[ “id” ], “secret_message_encrypted” : secret_message_encrypted , }, )
最后,当用户再次提供密码时解密消息的代码:
数据=等待请求。后变量() 密码=数据。得到( “密码” , “” )。带状() 如果不是密码: 返回等待_error ( 数据集,请求, “请提供密码” ,状态= 400 ) # 用密码解密私钥 尝试: private_key = decrypt_private_key_for_user (参与者,密码) 除了ValueError : return await _error (数据集,请求, “密码不正确” ,状态= 400 ) # 用私钥解密秘密信息 解密消息=私钥。解密( 参与者[ “secret_message_encrypted” ], 填充。环境规划署( mgf =填充。 MGF1 ( algorithm = hashes . SHA256 ()), 算法=哈希。 SHA256 (), 标签=无, ), ).解码( “utf-8” )
还有一些雪花
我花了五分钟的时间来设计它的视觉效果——它的主要特点是身体上有一个粗的红色顶部边框,然后是一个较薄的白色边框,让它看起来像是戴着圣诞老人的帽子。
不过我确实添加了一些动画雪花!我使用了娜塔莉·唐恩 (Natalie Downe) 于 2010 年编写的脚本。效果很好!
在 Glitch 上部署它
这种项目非常适合Glitch ,它提供免费托管和持久文件存储 – 非常适合 SQLite – 只要你不介意你的项目在活动之间休眠(除非你支付“提升”它们). Secret Santa 应用程序非常适合此类托管。
(您可以通过单击“重新混合”按钮重新混合我的项目以获取您自己的应用程序副本(使用您自己的数据库)。)
因为我已经将插件发送到 PyPI,所以将它部署到 Glitch 上就是在那里创建一个包含这个单一glitch.json
文件的新项目的问题:
{ “安装” : “ pip3 install --user datasette datasette-secret-santa -U ” , “开始” : “数据集——创建.data/santa.db -p 3000 ” }
这会导致 Glitch 在项目首次启动时同时安装datasette
和datasette-secret-santa
。然后它启动像这样运行的 Datasette 服务器:
datasette --create .data/santa.db -p 3000
--create
标志告诉 Datasette 创建一个新的 SQLite 数据库,如果该路径上尚不存在的话。 .data/
是 Glitch 上的一个特殊目录,不会使用其版本控制自动跟踪其内容。
-p 3000
标志告诉服务器侦听端口 3000,这是 Glitch 默认值 – 到应用程序子域的流量将自动路由到该端口。
而且数据库是公开的
有一点有点令人惊讶:包含所有数据(包括公钥和加密私钥)的 SQLite 表对任何有权访问该实例的人都是可见的!
再次重申,我绝不是密码学专家,对于任何其他应用程序我都不会容忍这种情况。但是考虑到秘密圣诞老人的风险状况,我认为这是可以的。我敢肯定,如果你真的愿意,你可以暴力破解私钥,所以他们没有被用于其他任何事情是件好事!
(这也是我不让用户选择他们自己的密码的原因之一——通过分配生成的密码,我可以 100% 确定我不会不小心最终持有可用于任何事情的凭证的加密副本别的。)
作为插件的独立应用程序
我觉得这个项目有趣的一点是它演示了如何使用 Datasette 插件来提供完整的、独立的应用程序。
我认为这是一个强大的模式。这是一种巧妙的方式,可以利用我构建的工具来帮助使 Datasette 易于部署——不仅在 Glitch 上,而且在 Fly 等平台上也是如此。
这是我第一次以这种方式使用 Datasette,我发现它是构建和部署这种个人工具的一种令人愉快的高效方式。我期待着在未来的其他项目中尝试这种方法。
如果您了解密码学并且可以发现我的系统工作方式中任何明显(或微妙)的漏洞,请打开一个问题让我知道!
原文: http://simonwillison.net/2022/Dec/11/over-engineering-secret-santa/#atom-everything