
简介本资源是一个基于Python实现的隐私保护电子投票系统聚焦同态加密算法在实际场景中的工程落地面向计算机专业本科生及研究生开展毕业设计、课程设计或科研项目开发。系统完整集成ElGamal半同态加密与整数环上的全同态加密方案支持密文状态下统计票数保障选民身份匿名性与投票内容机密性。压缩包共79个文件含24个核心Python源码覆盖密钥生成、投票、计票、视图展示等模块、17张界面与流程图PNG、4份PDF学术论文含Dijk2010全同态奠基文献及国内方案设计文档、1份SQL建库脚本及详细README.md说明文档整体仅1.97MB轻量易部署。目前已有33人学习下载提供可直接运行的测试通过代码、多密钥长度配置128/1024/2048位、清晰分层目录结构HE、Database、Vote、View等模块及配套数据库操作封装便于理解密码学原理并快速二次开发。 一个想保护投票者隐私的电子投票系统到底怎么落地我给出的方案是用Python实现同态加密让每一张选票在加密状态下完成聚合统计。整套系统从密钥生成、选票加密、同态计票到结果解密都走完了完整的流程源码和项目文档也已经整理好适合拿去做毕业设计、课程设计或者自己研究。这篇文章不聊虚的直接把项目拆开讲为什么投票必须用同态加密、算法怎么选、核心代码怎么写、实际跑起来会遇到哪些坑都一并放出来。如果你正在准备做一个类似的隐私保护系统或者只想搞明白“密文上直接做运算”到底是怎么实现的这篇文章可以帮你省下大量的调研和踩坑时间。1. 项目背景与整体设计思路1.1 为什么电子投票必须引入同态加密传统电子投票系统最大的隐患不在传输链路而在服务器端的裸数据。选票一旦以明文形式存储在数据库里管理员、数据库运维人员或者拿到服务器权限的攻击者都能直接看到每一个投票者投给了谁。这不仅仅是隐私泄露的问题更会反向影响投票的公正性——知道别人的票投给谁之后贿选、胁迫、打击报复都变得容易了。要解决这个问题最直接的想法是传输过程中用HTTPS数据库里做字段加密。但这里有个悖论如果选票是加密存储的计票的时候就必须先解密才能统计票数。解密之后所有选票又会以明文形式出现在内存里隐私保护就断了最后一环。所以仅仅做“静态加密”是不够的我们需要一种允许在密文上直接做加法运算的加密方式也就是同态加密。同态加密的价值在于选票从客户端提交开始就是密文服务器端只能看到一堆完全随机化的密文数据即使拿到数据库也分析不出任何投票倾向。但计票时服务器不需要解密每一张票直接在密文上做加法累加最后用私钥解密一次得到的就是所有票数的总和。整个过程里单张选票的内容从未以明文形式暴露过这才是真正意义上的隐私保护。1.2 系统架构与功能模块划分我设计这个系统时采用的是经典的B/S架构前端负责交互和选票采集后端跑核心业务逻辑和加密运算数据库负责持久化存储。为了方便部署和调试前端用了轻量级的HTMLJavaScript页面后端选择Python的Flask框架数据库用SQLite起步。整体模块划分如下用户认证模块投票者注册、登录、身份校验防止未授权访问。选举管理模块创建选举活动、设置候选人、配置投票起止时间。选票加密模块调用同态加密公钥对选票明文进行加密生成投票密文。同态计票模块对全部选票密文执行同态加法不接触明文数据。结果解密模块使用私钥对聚合后的密文解密输出最终票数。审计日志模块记录关键操作日志保证过程可追踪、可审计。模块之间的核心数据流是这样的投票者提交的原始选择先被编码成数值然后用公钥加密成密文存入数据库计票时直接读取密文列表逐个执行同态加法得到汇总密文后再用私钥解密一次。公钥和私钥分离是关键设计私钥一旦参与网络传输整个系统的信任基础就不成立了所以我把私钥放在独立的计票管理员手里不对业务服务器开放。1.3 技术选型为什么是Python加Paillier做技术选型时我认真比较过几个方向。全同态加密如BFV、CKKS功能强大理论上支持任意计算但工程实现复杂度高运算开销大Python环境下几乎没有开箱即用的轻量级方案。RSA虽然也具备一定的乘法同态性质但它的同态特性不适合直接做加法聚合语义安全性也不够。最终我选择了Paillier加密算法理由有三点第一Paillier支持加法同态投票计票本质上就是票数的累加两者天然契合。第二Python生态里有现成的phepython-paillier库封装质量高几百行代码就能完成密钥生成、加密、同态加法、解密的全流程。第三Paillier的安全性基于大整数分解难题经过多年学术验证在密码学领域是公认可靠的选择。Python的优雅之处在于你可以用极少的代码实现复杂的密码学逻辑把主要精力放在系统设计上。phe库内部已经实现了大数运算优化和处理细节对做课程设计或者毕业设计的同学来说是最稳妥的起点。2. 同态加密核心原理与原理解析2.1 一句话理解同态加密用最直白的方式讲同态加密就是一种“密文之间可以直接做运算运算结果解密后等于明文直接做运算的结果”的加密技术。打个比方普通的加密就像把物品锁进保险箱想看内容必须开锁同态加密则像一只特制的“加密手套”你可以带着手套对手里的东西做各种操作加、减、乘做完之后把手套摘掉看到的结果和你徒手操作得到的结果一模一样而且在整个过程中你始终不知道东西的真实形态。具体到投票场景就是服务器不需要知道张三投了谁、李四投了谁只需要在密文层面把所有的“加密选票”加在一起。因为加法同态的性质最终解密得到的数字就等于把所有选票的明文数值相加后的总和。这样一来单张选票的信息被完整隐藏统计结果却完全准确。2.2 Paillier加密算法的工作过程Paillier算法在实现上可以拆成四个步骤密钥生成、加密、同态加法、解密。先看密钥生成过程系统会生成两个大素数p和q计算n p * q再计算lambda lcm(p-1, q-1)。公钥是n私钥是lambda。实际使用时phe库把这些数学细节都封装好了只需要调用PaillierPublicKey.create()就可以拿到公钥对象再通过公钥对象的secret()方法生成私钥。加密的过程比较有趣。假设投票者选择的是候选人A系统把A映射为数值1然后从随机数空间里取一个随机数r计算密文c (1 n)^1 * r^n mod n^2。这里的随机数r非常关键它保证了同一个明文字1每次加密出来的密文都不同即使两个投票者都投了A他们数据库里的密文也完全不一样攻击者无法通过比对密文来判断投票倾向。同态加法是Paillier最迷人的地方。如果有两个密文c1和c2分别对应明文m1和m2那么c1 * c2 mod n^2 得到的新密文解密后等于m1 m2。也就是说密文相乘对应的操作本质上是明文相加。在代码里phe库重载了乘法运算符直接把两个密文对象相乘就能得到聚合结果。解密时只需要把聚合密文用私钥做一次数学变换就能还原出最终的明文总和。需要注意的是这里的“明文总和”是所有投票者数值之和。如果投票值编码成1表示投A、2表示投B那解密出来的就是一个混合数据所以编码方案必须精心设计我采用的是每个候选人都分配独立的计数向量这个细节后面会详细讲。2.3 安全性分析与密钥管理策略很多同学会担心Paillier的随机数r会不会削弱安全性恰恰相反正是随机数保证了语义安全性。所谓语义安全就是攻击者拥有两个明文和其中一个的密文也无法判断这个密文对应哪个明文。Paillier的随机化特性天然满足这一点这对于投票场景至关重要因为投票系统最大的风险之一就是密文比对攻击。密钥管理是这个项目里最需要认真对待的部分。公钥可以公开部署在业务服务器上任何投票者都可以用公钥加密自己的选票。私钥必须与业务服务器物理隔离只有负责最终计票的管理员才能持有。我在项目里还做了一个细节设计私钥通过密码加密后存储需要使用私钥时必须输入管理密码才能解密加载。这样即使私钥文件被窃取攻击者也拿不到实际的私钥内容。另外一个值得注意的点是同态加法有一个“噪声增长”的问题。每做一次密文加法密文的噪声都会增加当累加的密文数量足够多时噪声超过阈值解密就会出错。实际测试下来使用2048位的n值累加几百张票完全没有问题但如果要跑上万张票的大型选举就需要把n的位数提升到3072位甚至更高或者采用分层聚合的策略来降低噪声。3. 系统核心模块实现与关键步骤3.1 系统流程设计从选票生成到结果公布整个投票流程我设计了七个步骤每一环都必须严格串起来缺一不可。第一步是管理员创建选举活动配置好候选人列表和投票期限。第二步是投票者注册账号并通过身份审核。第三步是投票者登录系统获取当前正在进行中的选举信息。第四步是投票者做出选择前端将选择编码成对应的数值向量。第五步是客户端使用公钥加密数值向量生成选票密文提交给服务器。第六步是服务器校验投票者资格和重复投票状态通过后把密文入库。第七步是投票截止后管理员触发计票流程系统读取全部密文执行同态加法再用私钥解密得到最终结果。流程设计上有一个必须坚守的原则服务器永远不应该接触选票明文。也就是说加密操作必须发生在客户端或者可信的加密服务节点上不能由Web服务器直接加密。如果服务器能拿到明文所谓的隐私保护就形同虚设。我的实现方案是把加密逻辑封装成一个独立的加密服务模块前端通过API调用这个模块完成加密选票明文只存在于客户端内存和加密模块的运行内存中落地到数据库的永远是密文。3.2 核心功能模块详解登录、加密、计票三件套登录模块我采用了JWTJSON Web Token来管理用户会话。投票者输入账号密码后后端校验通过签发一个有效期为2小时的JWT。前端在后续请求中携带这个Token后端通过装饰器校验登录状态。这里要提醒一下JWT密钥不要硬编码在代码里应该从环境变量中读取。我把JWT密钥和私钥口令统一放进了配置文件部署时通过环境变量注入避免源码泄露导致安全兜底失效。加密模块是整个系统的灵魂。我定义了一个VoteCrypto类负责公钥加载、明文编码、选票加密和同态计票。这个类初始化时需要指定公钥文件路径和私钥文件路径为了性能考虑公钥和私钥对象只初始化一次后续加密、计票都复用实例。计票模块做的事情就是从数据库里捞出某个选举活动下的全部选票密文然后对每个候选人的计数分量分别做同态加法。这里有个容易出错的点密文不能直接存成整型丢进数据库因为Paillier生成的密文是一个很大的整数直接存成TEXT字段比较稳妥。我用的是把密文对象转换成十六进制字符串的存储方式读取时再解析回加密数字对象。3.3 选票编码方案一人投多个候选人的处理技巧刚开始设计的时候我遇到一个棘手的问题Paillier加法同态只能对数值做累加但一场选举往往有多个候选人投票者只能选其中一个怎么用数值编码表示“选A不选B”最初的方案是单值编码把候选人A映射为1、B映射为2、C映射为3提交后汇总解密得到的数字是一个总和完全无法拆分成每个候选人的票数。这个方案直接废弃了。后来我换成了向量编码方案。假设一场选举有3位候选人投票者的选择用一个长度为3的整数向量表示投给谁就在对应的位置上置1其他位置置0。比如选B向量就是[0, 1, 0]。加密时对向量的每个分量分别加密得到一个密文向量。计票时把所有人的密文向量做逐分量的同态加法得到一个汇总密文向量。最后解密第一个分量就是候选人A的总票数第二个分量就是B的总票数。这种编码方式的好处非常直观每个候选人的票数独立统计不会互相污染而且密文向量的长度只取决于候选人数量扩展性很好。不过代价也很明显加密时间和密文存储量会随候选人数量线性增长。候选人数量在10人以内时性能完全可接受。4. 关键代码实践核心环节实现4.1 密钥生成模块实现# key_generator.py from phe import paillier def generate_keypair(n_length2048): 生成Paillier公私钥对 :param n_length: 模数n的位数默认2048位 :return: (公钥, 私钥) pub_key, priv_key paillier.generate_paillier_keypair(n_lengthn_length) return pub_key, priv_key这里要特别注意n_length的选择。我最初测试时用了1024位加密速度快但安全性不够论文里都不太好写。后来改成2048位速度仍在可接受范围内。如果选举规模大建议直接用3072位安全性更稳健。保存密钥对象时不能直接用pickle序列化因为密钥文件一旦泄露私钥安全性就无法保证。我做了两层处理第一层给密钥文件设置600权限只允许管理员账户读写第二层私钥内容用AES加密后再写盘加密口令从环境变量读取。4.2 选票加密与同态计票实现# vote_crypto.py import json from phe import PaillierPublicKey, PaillierPrivateKey class VoteCrypto: def __init__(self, pub_key_path, priv_key_pathNone): self.public_key self._load_public_key(pub_key_path) self.private_key self._load_private_key(priv_key_path) if priv_key_path else None def encrypt_vote(self, vote_vector): 加密选票向量 :param vote_vector: 例 [0, 1, 0] 表示投给第二位候选人 :return: 密文向量EncryptedNumber列表 encrypted_vector [ self.public_key.encrypt(value) for value in vote_vector ] return encrypted_vector def serialize_ciphertext(self, encrypted_vector): 将密文向量序列化为可存储的字符串 return [ { ciphertext: str(enc.num), exponent: enc.exponent } for enc in encrypted_vector ] def deserialize_ciphertext(self, data_list): 从存储字符串恢复密文向量 result [] for item in data_list: enc self.public_key.encrypt(0) # 占位便于复用已有对象 enc._EncryptedNumber__ciphertext item[ciphertext] enc._EncryptedNumber__exponent item[exponent] result.append(enc) return result def tally_votes(self, ciphertext_vectors): 同态计票对每个候选人的密文分量分别累加 :param ciphertext_vectors: 所有选票密文向量的列表 :return: 聚合后的密文向量 if not ciphertext_vectors: return None vec_len len(ciphertext_vectors[0]) tally_vector [] for i in range(vec_len): # 从第一张票的对应分量开始累加 acc ciphertext_vectors[0][i] for j in range(1, len(ciphertext_vectors)): acc acc ciphertext_vectors[j][i] tally_vector.append(acc) return tally_vector def decrypt_result(self, tally_vector): 解密最终统计结果 return [self.private_key.decrypt(enc) for enc in tally_vector]这段实现里有几个细节值得展开说明。第一serialize_ciphertext函数保存了密文本体和指数。paillier库中EncryptedNumber对象存放的是明文 * (加密基数)^exponent的乘积如果exponent不是0做同态加法时必须保持所有密文的exponent一致否则结果会出错。为了方便我在加密时统一不设置exponent默认0但序列化时仍然保存这个字段避免将来扩展出现兼容性问题。第二同态加法用符号而不是*。你看到代码里acc acc ciphertext_vectors[j][i]这里真正执行的是密文的乘法操作但phe库重载了运算符对外表现为“同态加”。所以阅读代码时要注意不要被运算符表面骗了。第三解密前一定要确保数据已经完成了全部的同态加法。我在项目里加了状态字段只有投票状态为“结束”的选举才能触发解密操作避免中途解密造成隐私泄露。4.3 数据库设计与存储方案数据库用的是SQLite表结构我设计了四张表users、elections、candidates、votes。users存储投票者账号信息包含用户名和密码哈希。elections存储选举活动包含标题、开始时间、结束时间、状态。candidates存储候选人信息通过外键关联选举活动。votes表是核心存储每张选票的密文。votes表的设计有一个关键点我不能存一个“密文字符串”字段就完事因为密文向量有多个分量。我的做法是把整个密文向量序列化成一个JSON字符串存进一个TEXT字段同时加一个vote_hash字段作为唯一性校验确保同一个投票者不能重复投票。vote_hash生成方式是对投票者ID和选举ID做拼接后取SHA256再加唯一索引。CREATE TABLE votes ( id INTEGER PRIMARY KEY AUTOINCREMENT, voter_id INTEGER NOT NULL, election_id INTEGER NOT NULL, encrypted_vector TEXT NOT NULL, vote_hash TEXT NOT NULL UNIQUE, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (voter_id) REFERENCES users(id), FOREIGN KEY (election_id) REFERENCES elections(id) );如果投票者是匿名的可以不要voter_id字段改成生成一个一次性投票凭证。我用的是实名制投票模型所以voter_id保留配合JWT会话能有效防止重复投票和刷票行为。4.4 服务端API接口设计Flask后端我设计了7个API接口对应的功能分别是注册、登录、创建选举、获取选举列表、提交选票、触发计票、获取结果。我挑两个关键的接口说下实现思路。提交选票接口是整个链路上最重要的一道防线app.route(/api/vote/submit, methods[POST]) login_required def submit_vote(): data request.get_json() voter_id g.user_id election_id data.get(election_id) encrypted_vector data.get(encrypted_vector) # 前端加密后传输的密文向量 # 检查选举是否存在且在有效期内 election get_election_by_id(election_id) if not election or election.status ! ongoing: return jsonify({code: 4001, message: 选举不在进行中}), 400 # 检查是否已投票 vote_hash hashlib.sha256(f{voter_id}:{election_id}.encode()).hexdigest() if get_vote_by_hash(vote_hash): return jsonify({code: 4002, message: 请勿重复投票}), 400 # 校验密文向量的长度是否与候选人数量一致 if len(encrypted_vector) ! get_candidate_count(election_id): return jsonify({code: 4003, message: 选票数据不合法}), 400 # 存储密文 insert_vote(voter_id, election_id, json.dumps(encrypted_vector), vote_hash) return jsonify({code: 0, message: 投票成功}), 200这里我没有对密文内容做额外的合法性校验比如校验分量值是不是0或1。原因很简单服务器拿到的是密文做不了明文校验。想要确保投票者没有篡改选票编码需要在协议层引入零知识证明这是进阶研究方向。对课程设计来说保证“密文来自合法客户端”已经够用。5. 常见问题排查与项目经验总结5.1 高频异常与解决方案我实跑过程中遇到过不少问题最典型的有这么几个。第一个坑是“密文无法解密”。排查一圈后发现是同态加法次数太多导致噪声超过了解密阈值。解决办法是加大n_length到3072位同时测试时把加密批量处理减少不必要的密文值调整。如果你要计上万张票建议把选票先按小组聚合再对小组结果做二次聚合有效降低单次累加次数。第二个坑是“数据库存密文时取整错误”。直接对EncryptedNumber对象调用int()或者str()再把结果存数据库有时候会因为精度问题导致数据不一致。正确做法是用我上面的序列化方式保存ciphertext和exponent两个字段读取时再构造EncryptedNumber对象。我在项目文档里特别提醒了这一点。第三个坑比较隐晦是Flask路由函数的并发问题。由于phe库的公钥对象不是线程安全的高并发下多个投票请求同时做加密会导致部分密文计算错误。我做了两个处理一是给加密服务加了一个全局锁确保同一时间只有一个线程执行加密操作二是部署时用Gunicorn的同步worker模式每个worker进程独立持有一份公钥对象。第四个坑来自测试阶段因为直接编辑数据库导致选举状态混乱。后来我统一封装了数据访问层禁止在业务逻辑里直接写SQL。测试时用单独的测试库开发库和测试库完全隔离。5.2 性能优化与安全加固建议如果你只是跑通毕业设计默认配置完全够用。但如果要处理成百上千人同时投票下面这些优化建议值得参考。加密性能是最明显的瓶颈。Paillier加密是大整数模幂运算非常耗时。我的测试环境是普通笔记本加密一个长度为5的选票向量耗时大约0.5秒。如果5000人同时投票理论耗时超过40分钟。优化方案有两个一是把加密操作从同步改为异步前端提交后立即返回“受理成功”后台排队加密处理二是用并发加密适当增加进程数因为Python的多线程受GIL限制这里更适合用多进程。数据库层面votes表是写入密集的SQLite的锁机制在高并发下会暴露问题。建议从select方法查询变更为WAL模式并打开busy_timeout。如果系统规模再大一点就要考虑换MySQL或PostgreSQL了。安全加固方面我强烈建议你给项目加上HTTPS。本机跑HTTP没有关系但部署到公网环境后如果选票密文在传输中被篡改会影响计票结果的准确性。另外我还在接口入口做了简单的速率限制防止恶意刷票同时还加了操作审计日志每一次投票、计票、解密操作都会记录操作者、时间、IP地址。5.3 项目扩展方向思考做完这个项目之后我认真想过它的后续演进空间。一个很自然的扩展是引入数字签名机制投票者在提交选票密文的同时用自己持有的私钥对密文做签名服务器验证签名后才接受选票。这样可以防止攻击者伪造投票请求同时还能保证选票的不可抵赖性。另一个扩展方向是零知识证明。目前的系统里服务器虽然接触不到明文选票但恶意投票者可以构造一个不合法的密文比如把向量编码成[1, 1, 1]造成所有候选人票数异常。通过引入区间证明或范围证明投票者可以在不泄露明文内容的前提下向服务器证明自己的选票确实只投给了唯一的候选人。这个方向很有研究价值做毕业设计的话可以拿来当创新点。最后如果你想往学术方向靠可以把Paillier替换成BFV或者CKKS方案支持更多类型的同态计算。不过这会让系统复杂度明显上升Python生态里可用的库也比较少通常需要结合C扩展或者SEAL库来用。坦率说对于课程设计来说Paillier方案已经能很好地体现同态加密的核心思想没有必要一开始就上重型方案。我最后再分享一个经验做这类隐私保护系统编码方案和密钥管理设计初期一定要先用文档定下来不要急于写代码。我第一次就是直接上手后来发现编码方案设计错了数据库里的测试数据全废只能重新设计重写。方案定了之后再动手后面写代码和调试都能顺畅很多。希望这个项目拆解能帮到你也祝你的投票系统早日跑通全流程。本文还有配套的精品资源点击获取