十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python的同态加密电子投票系统:Paillier算法与隐私保护实践

基于Python的同态加密电子投票系统:Paillier算法与隐私保护实践 简介本资源是一个基于Python实现的隐私保护电子投票系统聚焦同态加密算法在实际场景中的工程落地面向计算机专业本科生及研究生开展毕业设计、课程设计或科研项目开发。系统完整集成ElGamal半同态加密与整数环上全同态加密方案支持密文状态下统计票数保障选民身份匿名性与投票内容机密性。压缩包共79个文件含24个核心Python源码覆盖密钥生成、投票、计票、视图展示等模块、17张界面与流程图PNG、4份PDF学术论文含Dijk2010全同态奠基文献、1份SQL建库脚本及详细README.md项目说明整体仅1.97MB轻量易部署。目前已有33人学习下载配套文档清晰标注各模块职责与调用关系源码经严格测试可直接运行并预留扩展接口便于算法替换或功能增强是理解密码学应用与安全系统架构的优质实践范例。 毕业设计里但凡沾上隐私保护电子投票这类关键词十有八九会被引导到同态加密这个方向上来。加上Python做落地既不用跟C底层密码库死磕又能把算法流程讲清楚作为课程设计或本科毕设这个选题可以说是看起来高级、做起来可行、答辩有得聊的典型代表。不过很多同学在真正动手时才会发现同态加密和普通加密完全是两种思维前者要处理的不只是加密这个动作而是一整套在密文上做运算的流程。这篇内容我会从需求拆解、算法原理、系统设计、核心代码、常见坑位几个维度完整过一遍尽量把踩过的坑和该提前规划的点都写出来。1. 项目背景与核心需求拆解1.1 为什么同态加密和电子投票总被放在一起先理解题目里的核心矛盾电子投票系统要解决线上投票的便利性问题但传统的加密方式比如AES、RSA只保证数据在传输和存储过程中不被偷看一旦需要统计票数就必须先把密文解开成明文然后才能做加法计数。这个先解密再统计的过程里明文数据会暴露给系统内部人员也会给攻击者一个明确的攻击窗口——如果数据库被拖走只要私钥泄露所有选票立刻曝光。同态加密则提供了一种更优雅的路径在密文上直接做加法或乘法运算得到的结果解密后就是明文做同样运算的结果。也就是说选票可以全程以密文形式存放在服务器上计票时也不需要解密每一张票只需要把所有密文做同态累加最后只解密一个总票数结果。这样一来计票员看不到任何单张选票的内容隐私保护的强度从系统约定不去看升级成数学上根本看不到。从项目选题的角度看这个组合自带天然的合理性既有一个足够有分量的安全算法同态加密又有一个直观易懂的应用场景投票还暗含了一组值得论证的安全需求机密性、完整性、合法性、可验证性。做这个项目的过程中你既可以说算法也可以说系统设计还可以聊工程实现答辩和评阅时几乎不用担心无话可讲。1.2 电子投票系统需要守住哪些底线要做一个参与方都信服的电子投票系统光有加密是不够的。拆开看至少要满足四条安全需求保密性任何人包括系统管理员都无法看到单个选民的投票内容密文在传输和存储过程中不泄露信息。完整性选票一旦提交不能被篡改计票结果必须和实际收到的合法票一致。合法性只有合法选民能投票且一人最多投一次不能重复投票或伪造他人身份投票。可验证性至少要能让系统在计票后向所有人证明这个结果是票箱里那些密文的正确统计而不是管理员手动填上去的数字。这四条需求对系统架构有直接影响。比如要求合法性就需要一个选民认证模块常见的做法是用户名密码或一次性令牌要求一人一票数据库里就要有唯一性约束或者用密码学手段比如盲签名在服务器不掌握投票内容的前提下分发投票权要求可验证性就要在系统中记录每张选票的密文并公开所有密文列表允许第三方重复执行同态累加并比较结果。很多初做这个项目的人只盯着同态加密这个点结果答辩时被问如何防止重复投票就答不上来。这里建议在系统设计阶段就把这四条需求列成一张表每一条对应到具体的模块或技术方案整个系统才不会出现明显的逻辑漏洞。1.3 技术选型为什么是Python Paillier同态加密听起来高大上但真正成熟的全同态方案比如基于格的BFV、CKKS在工程上非常复杂参数选择、密钥管理、性能调优对初学者极不友好。对课程设计和本科毕设来说通常推荐选择部分同态加密里的Paillier算法它天然支持密文加法也就是同态加法和密文与明文之间的标量乘法。而电子投票的计票过程本质上就是把所有选票加总正好是Paillier的擅长领域。选择Python的原因更直白生态成熟、开发速度快、调试验证方便。Python的gmpy2库可以做大整数的快速模幂运算phe库直接封装了Paillier的加解密就算你想手写算法核心Python代码的表述也比C容易理解得多。另外Flask可以在两小时内搭出一个有网页界面的投票前台让期末演示效果直接拉满。需要注意Python在大整数性能上确实不如C但在Paillier中密钥长度设到1024位p、q各512位时单次加密在普通笔记本上也就几毫秒到几十毫秒几千张票完全扛得住。如果做1万人的并发投票Flask单进程会吃力但对演示和课程设计已经绰绰有余。2. 系统设计与同态加密原理2.1 整体业务流程与角色设计一个典型的使用Paillier的电子投票系统可以拆成四类角色选民主体Voter登录系统、获取选票、提交加密后的投票密文。认证中心Auth验证选民身份、保证一人一票。在课程设计里可以简化成用户表和登录态想做得更严谨可以用盲签名方案让认证中心在不知道投票内容的情况下给选票签名。计票中心Tally持有公钥收集所有选民的选票密文对密文做同态累加然后交给解密方解密出总票数。计票中心理论上不应该持有私钥。解密机构Decryptor持有私钥接收累加后的密文解密得到最终结果。这里最关键的架构决策是私钥绝不能和选票库放在同一台服务器上。如果做单机版的课程设计为了演示方便很多同学把私钥就存在项目目录里这样虽然能跑通但在文档里一定要明确说明这是演示环境的简化做法真实系统中应拆分权限、用门限解密把私钥分为多份多方合作才能解密。标准业务流程可以这样设计选民注册并获得合法的投票资格这里用用户名密码登录即可。服务器为当前投票活动生成Paillier密钥对公钥公开给所有选民私钥由解密机构持有。选民在投票页面选择候选人前端或后端把选项编码成整数比如投给A记为1、B记为2、弃权记为0然后用公钥加密得到选票密文。选票密文提交到服务器写入数据库同时记录选民的投票状态防止重复投票。投票结束后计票模块从数据库读出所有密文按照候选人类型分成若干组组内执行同态加法运算。把各组累加后的密文交给解密机构解密得到每个候选人得到的票数展示结果。这个流程里同态加密发挥作用的环节在第3步到第5步第3步加密选票第5步密文累加第6步只解密最终总数。整个过程中任何管理员都看不到某个选民具体投给了谁。2.2 Paillier同态加密用大白话说透Paillier算法之所以能实现加密状态下做加法核心在于模数运算的一些特殊性质。直接看公式可能会头晕但拆开来看其实就四步。第一步密钥生成。随机选取两个大素数p和q计算n p * q同时计算λ lcm(p-1, q-1)也就是两者最小公倍数。再选一个生成元g通常可以取g n 1这个取值能简化很多计算。最后计算μ它的公式为 μ (L(g^λ mod n²))⁻¹ mod n其中函数L(x) (x - 1) / n。最终公钥是(n, g)私钥是(λ, μ)。这里面所有运算都在模n²的空间中进行这也是Paillier密文比RSA密文胖一倍的原因。第二步加密。对明文消息m0 ≤ m n随机选取一个r1 ≤ r n且r与n互质计算密文 c g^m · r^n mod n²。这个r是每次加密时随机变化的所以同一个明文每次加密得到的结果都不同这保证了选票密文不会被攻击者通过对比密文猜出内容。第三步解密。拿到密文c后计算 m L(c^λ mod n²) · μ mod n就能还原出明文m。解密过程只需要私钥(λ, μ)。第四步同态加法。如果有两个密文c₁ E(m₁)、c₂ E(m₂)那么计算 c c₁ · c₂ mod n²得到的结果解密后正好是 m₁ m₂。同样的道理如果要统计同一个候选人的全部票数只需要把这些选票密文逐个相乘在模n²意义下得到的最终密文解密后就是总票数。用一个生活化的类比普通加密就像把每张纸币放进一个独立的保险箱要算总额必须把所有保险箱打开、把钱拿出来数同态加密则像把每张纸币放进一个带透明计数功能的特殊信封信封叠在一起外面就能看到总额但看不到各自里面是多少。每个信封的加密状态始终没有被破坏。2.3 模块划分与数据库设计从代码工程的角度这个系统的模块划分可以很清爽。建议目录结构如下voting_system/ ├── app.py # Flask主入口路由与页面渲染 ├── crypto/ │ ├── __init__.py │ ├── paillier.py # Paillier算法的核心实现密钥生成、加密、解密、同态累加 │ └── key_manager.py # 密钥的生成、保存、读取管理 ├── db/ │ ├── __init__.py │ └── models.py # SQLAlchemy数据模型 ├── services/ │ ├── vote_service.py # 投票业务逻辑 │ └── tally_service.py # 计票业务逻辑 ├── templates/ # Flask前端页面模板 │ ├── login.html │ ├── vote.html │ └── result.html ├── static/ # 静态文件 ├── tests/ │ └── test_paillier.py # 算法单元测试 ├── docs/ │ └── 项目文档.md # 设计文档/论文素材 └── requirements.txt数据库设计也不复杂最少两张表users表id、username、password_hash用werkzeug的加密哈希不要存明文、voted_flag是否已投票。ballots表id、user_id、candidate_id本次选了谁、ciphertext存储加密后的选票密文格式为十六进制或Base64字符串、created_at。如果你想同时支持多个候选人一种直观的设计是每个候选人在ballots表里对应一条记录但更常见的做法是每个候选人一个计数器投票时如果选民投给候选人A那么就在A的计数器上加密加1B和C的计数器上加密加0。这样计票时只需要分别累加A、B、C各自的加密计数器。不过这种设计对单个选民投多个候选人的场景不够灵活针对课程设计来说最简单可靠的方式是一个选民一条投票记录记录投给了谁计票时按照候选人分组然后对组内的选票密文做同态累加。这里有个容易忽略的细节如果用投A记为1投B记为2的方式编码那把所有选票累加后得到的数字是混乱的A的票数和B的票数分不开。所以优先推荐每个候选人一个加密计数器的方式或者按候选人分组后再分别累加这样语义清晰代码也不容易写错。3. 核心代码实现从密钥生成到计票解密3.1 环境准备与工程初始化先把Python环境准备好建议用Python 3.9以上版本。需要的第三方库主要有pip install flask flask-sqlalchemy gmpy2 phe其中phe是Paillier的官方Python库可以直接调用paillier.EncryptedNumber等类型gmpy2能显著加速大整数运算。如果你打算自己手写Paillier算法核心有些学校会要求展示算法实现细节那gmpy2基本是必备的它的powmod、invert、lcm函数比Python内置的大整数运算快一个数量级。工程初始化时建议先写一个config.py统一管理配置项import os class Config: SECRET_KEY os.environ.get(SECRET_KEY, dev-secret-key) SQLALCHEMY_DATABASE_URI sqlite:///voting.db SQLALCHEMY_TRACK_MODIFICATIONS False # Paillier密钥长度课程设计建议1024论文实验建议2048 KEY_SIZE_BITS 1024密钥长度选择上Paillier的安全性依赖大整数分解的困难性。1024位的n意味着p和q各512位能应对课程设计的安全演示如果论文里要谈安全性分析建议把n设为2048位但相应的加解密耗时和密文长度都会上升。我在实验里测过2048位密钥下单次加密约30ms解密约15ms对几千张选票完全不是问题。3.2 Paillier工具类自己写一遍也封装一层虽然phe库可以直接用但既然是同态加密相关的项目建议自己把核心算法写一遍一方面方便在论文里展示算法流程另一方面也方便理解后面遇到的坑。封装一个crypto/paillier.py实现最核心的四个功能密钥生成、加密、解密、同态累加。import gmpy2 from gmpy2 import mpz, powmod, invert, lcm import random def generate_keypair(bits512): 生成Paillier密钥对bits是单个素数的位长 p gmpy2.next_prime(random.getrandbits(bits)) q gmpy2.next_prime(random.getrandbits(bits)) n p * q n_sq n * n lam lcm(p - 1, q - 1) g n 1 # 简化取g n 1 # 计算 mu (L(g^lambda mod n^2))^{-1} mod n x powmod(g, lam, n_sq) L_x (x - 1) // n mu invert(L_x, n) public_key (n, g) private_key (lam, mu, n) return public_key, private_key def encrypt(public_key, plaintext): 加密明文返回密文整数 n, g public_key n_sq n * n r random.randrange(1, n) # c g^m * r^n mod n^2 ciphertext (powmod(g, plaintext, n_sq) * powmod(r, n, n_sq)) % n_sq return ciphertext def decrypt(private_key, ciphertext): 解密密文返回明文整数 lam, mu, n private_key n_sq n * n x powmod(ciphertext, lam, n_sq) L_x (x - 1) // n plaintext (L_x * mu) % n return plaintext def add_cipher(public_key, ciphertexts): 同态累加多个密文返回累加后的密文 n, _ public_key n_sq n * n result 1 for ct in ciphertexts: result (result * ct) % n_sq return result这段代码里有两个细节值得解释一下。为什么取g n 1因为二项式展开后可以得到g^m ≡ 1 m·n (mod n²)而且乘方运算结果依然在模n²空间中这样随机数r就会成为密文随机性来源解密时r的影响会被λ次方消掉。教科书里经常用随机选择一个满足条件的g但实际工程里取n1是标准做法实现简单且性能更好。为什么解密需要L(x) (x - 1) // n这是Paillier论文中的核心构造在模n²的循环群里取λ次方后密文中的随机数部分r^(n·λ)会映射到1而明文部分则会留在L函数的结果里。这个整数除法在gmpy2中就是//运算符但要注意Python内置的//对负数和大整数的行为需要保证整除性好在gmpy2的mpz类型不会出这种问题。3.3 投票流程中的加密与入库在实际的Flask应用里投票流程可以用一个接口来实现。前端页面把选中的候选人id通过POST请求传到后端后端在服务层完成读取候选人编码 - 调用加密 - 保存密文 - 标记已投票四个动作。关键代码如下from flask import Blueprint, request, jsonify, session from crypto.paillier import encrypt from db.models import db, User, Ballot from crypto.key_manager import load_public_key vote_bp Blueprint(vote, __name__) vote_bp.route(/api/vote, methods[POST]) def vote(): user_id session.get(user_id) if not user_id: return jsonify({code: 401, msg: 未登录}), 401 candidate_id request.json.get(candidate_id) if candidate_id not in [1, 2, 3]: return jsonify({code: 400, msg: 非法候选人}), 400 user User.query.get(user_id) if user.voted_flag: return jsonify({code: 403, msg: 您已经投过票了}), 403 public_key load_public_key() # 候选人编号作为明文加密 plaintext int(candidate_id) ciphertext encrypt(public_key, plaintext) ballot Ballot( user_iduser_id, candidate_idcandidate_id, ciphertext_hexformat(ciphertext, x), ) user.voted_flag True db.session.add(ballot) db.session.commit() return jsonify({code: 200, msg: 投票成功})注意这里的密文存储格式Paillier密文是一个大整数转成十六进制字符串后1024位密钥对应的密文大约256个十六进制字符作为数据库字段存储绰绰有余。不要直接存Python的int对象存成字符串才能持久化到SQLite。如果想要更强的隐私保护可以在前端用JavaScript调用一个加密函数让选票在浏览器端就完成加密后端只收到密文连候选人编号的明文都不接触。但这种方案需要在Web环境下引入gmpy2的JS版本实现复杂度明显上升。对于课程设计来说后端加密然后文档里讨论前端加密是进一步优化方向已经足够。3.4 计票流程中的同态累加与解密投票截止后计票模块做的事情其实很简单把数据库里所有选票密文按候选人分组组内做同态累加然后把累加结果解密。from crypto.paillier import decrypt, add_cipher from crypto.key_manager import load_private_key, load_public_key from db.models import Ballot def tally_votes(): public_key load_public_key() private_key load_private_key() ballots Ballot.query.all() # 按候选人id分组 groups {} for b in ballots: groups.setdefault(b.candidate_id, []).append(int(b.ciphertext_hex, 16)) results {} for candidate_id, cts in groups.items(): if cts: aggregated add_cipher(public_key, cts) total decrypt(private_key, aggregated) results[candidate_id] int(total) else: results[candidate_id] 0 return results这段代码在逻辑上很直观但有一个隐藏问题如果某个候选人的分组里密文数量为0那add_cipher的结果会是1因为初始化result1解密出来也不是0。解决方法是空组直接判定为0票不用走同态累加。还有一个更微妙的编码问题。按照上面候选人编号作为明文的方式如果两位选民分别投了候选人1和候选人2那么累加后得到的明文是3这里的3代表的是候选人1的票数加上候选人2的票数没有任何统计意义。所以计票必须按候选人分组分别累加千万不能把所有票混在一起算。要让密文累加得到总票数的语义成立更严谨的做法是每个候选人维护一个独立的加密计数器投票给候选人A时对A的计数器加密加1同时对B、C的计数器加密加0。计票时各计数器独立累加互不干扰。这种方法在数据库里就不存在选票明文字段隐私性更强但业务逻辑更复杂需要在每个候选人对应的表字段上做同态加法。[\text{整体流程图已经整理成文放在代码仓库README里这里不画图大家顺着文字流程就能跑通。}]4. 常见问题与排查技巧实录4.1 代码层面的高频坑做这个项目最容易踩的坑第一个就是解密结果和明文对不上。排查思路依次是检查密钥位数。p和q必须互不相同否则n的因数分解就会暴露安全性崩塌密钥生成时最好加一个if p q:的校验分支。检查密文读取方式。数据库里存的是十六进制字符串读取后必须int(x, 16)转回整数我曾经因为忘了转换直接把字符串传给解密函数结果在powmod里抛TypeError。检查模数范围。加密时明文m必须满足0 m n如果你把候选人id编码成了负数或者超过n的大整数解密结果必然错乱。第二个高频坑是同态加法的时候把密文当普通整数做加法。Paillier同态加法的实现是密文乘法不是密文加法。也就是说E(m₁) E(m₂)在数学上不等于E(m₁m₂)E(m₁) × E(m₂)才是E(m₁m₂)。初写代码时很容易下意识地把两个密文int值直接相加导致最后解密结果完全是垃圾数据。这一点一定要在代码注释里写清楚也建议在单元测试里专门留一个用例验证同态性质。第三个坑和并发有关。Flask默认是多线程处理的如果两个选民同时提交投票可能在读取user.voted_flag和更新数据库之间产生竞态条件导致同一用户投两次票。对课程设计来说最简单的解法是给users表的id字段加唯一约束同时把标记已投票和插入选票放在同一个数据库事务中。高级一点的做法是用Redis分布式锁但演示环境不太需要。4.2 性能瓶颈与参数调优如果做性能测试你会发现Paillier的加密过程在2048位密钥下仍然有明显耗时这在批量注册、并发投票时会造成压力。几个可行的优化方向将密钥长度降到1024位或者在论文实验部分说明密钥长度对性能的影响这是学术上很常见的一种对比方法。使用gmpy2代替Python内置的pow性能提升极其明显。同样的加密操作内置pow需要几百毫秒gmpy2.powmod几十毫秒搞定。批量加密时用多进程而不是多线程。因为gmpy2的GIL释放情况并不理想多线程加速有限multiprocessing.Pool反而能看到接近线性的提升。预生成随机数r。Paillier加密的随机数选择并不依赖明文可以提前生成一批随机数加密时直接取用减少随机数生成的开销。计票阶段如果选票量特别大分组累加其实可以先并行处理再汇总。比如把候选人A的密文分成多个子集分别计算子集的同态乘积最后再把这些中间结果乘在一起解密结果不变。这个思路在方案设计部分写出来会显得你对工程细节有深入思考。4.3 同态加密的真实边界与安全性探讨很多同学写完系统后最怕的一个问题是这套系统真的安全吗这里值得把它的安全边界讲清楚明白这点能让你的答辩更有说服力。Paillier的同态加密解决的是统计过程中的隐私泄漏问题但它本身不能解决投票者是否被胁迫“选票是否被恶意构造”等问题。比如一个选民警告了私钥的随机数r就可以证明自己投了谁或者一个恶意选民在加密时故意把明文设为非常大的数导致解密结果溢出扰乱统计。这些都需要额外的密码学工具去补充防胁迫Coercion Resistance需要更复杂的协议比如再加密混洗或可否认加密通常超出本科毕设的范畴。防恶意构造选票需要零知识证明选民在加密时要附带一个范围证明证明明文真的是0、1或某个合法候选人编号而不是一个破坏统计的数。Paillier可以在不暴露明文的情况下生成范围证明但实现难度较高可作为系统设计文档里的安全增强方向来写。可验证性方面可以公开所有选票密文和计票时的中间运算记录任何第三方都能用相同算法验证最终结果。关于私钥管理如果整个系统只有一个私钥且放在服务器上那系统管理员仍然可以解密任何选票。一个更合理的演示做法是采用门限解密把私钥用Shamir秘密共享拆成三份分别交给三个不同角色只有当三个人都同意时才能解密最终票数。这个方案实现不难却会让系统在架构上提升一个档次。4.4 演示与答辩时需要注意的细节最后聊聊展示环节。课程设计或毕业设计答辩老师通常不会深入看你看了多少行代码但一定会关心几个点第一演示前一定重新初始化一遍数据库并重新生成密钥对。如果拿之前测过的旧库展示极有可能出现候选人票数里残留测试数据一眼就能看出系统管理混乱。第二准备一个防重复投票的演示脚本同一个选民连续投两次界面必须明确提示您已投过票。这一条比同态加密本身更能体现系统完整性。第三准备好一张性能对照表。比如分别记录512位、1024位、2048位密钥下加密和解密的耗时再记录100张、1000张、5000张选票的计票耗时。答辩时把这张表放在PPT里能直观证明你做过实验、对算法复杂度有概念。第四把私钥不在计票服务器上这一点在系统设计图和展示页面中体现出来。即使你的代码是单机版也要在文档中明确标注教学演示与真实系统的区别避免答辩老师误以为你混淆了安全假设。我在实际做这个项目时最深的体会是同态加密的核心并不在会不会调库而在于你是否想清楚了密文在哪里流转、谁在什么时候能看见什么这个问题。只要把数据流画清楚把安全边界说清楚再复杂的算法也只是一个模块。而对用户来说真正能跑起来、能演示、能讲明白的系统才是一个好项目。最后再分享一个让导师眼前一亮的小技巧在计票结果页除了展示每个候选人的票数再加上一个同态校验按钮——系统把所有选票密文拉出来当你点击时在前端重新执行一次同态累加并与服务端结果比对一致时显示绿色验证通过。这一功能本身不复杂但向评审直观传递了一个信息这个系统不是我信你而是你随时可以查。这个小亮点往往比堆一堆密码学名词更管用。本文还有配套的精品资源点击获取
返回列表