简介:本资源是一个基于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架构,前端负责交互和选票采集,后端跑核心业务逻辑和加密运算,数据库负责持久化存储。为了方便部署和调试,前端用了轻量级的HTML+JavaScript页面,后端选择Python的Flask框架,数据库用SQLite起步。整体模块划分如下:
- 用户认证模块:投票者注册、登录、身份校验,防止未授权访问。
- 选举管理模块:创建选举活动、设置候选人、配置投票起止时间。
- 选票加密模块:调用同态加密公钥对选票明文进行加密,生成投票密文。
- 同态计票模块:对全部选票密文执行同态加法,不接触明文数据。
- 结果解密模块:使用私钥对聚合后的密文解密,输出最终票数。
- 审计日志模块:记录关键操作日志,保证过程可追踪、可审计。
模块之间的核心数据流是这样的:投票者提交的原始选择先被编码成数值,然后用公钥加密成密文,存入数据库;计票时直接读取密文列表,逐个执行同态加法;得到汇总密文后,再用私钥解密一次。公钥和私钥分离是关键设计,私钥一旦参与网络传输,整个系统的信任基础就不成立了,所以我把私钥放在独立的计票管理员手里,不对业务服务器开放。
1.3 技术选型:为什么是Python加Paillier
做技术选型时,我认真比较过几个方向。全同态加密(如BFV、CKKS)功能强大,理论上支持任意计算,但工程实现复杂度高,运算开销大,Python环境下几乎没有开箱即用的轻量级方案。RSA虽然也具备一定的乘法同态性质,但它的同态特性不适合直接做加法聚合,语义安全性也不够。最终我选择了Paillier加密算法,理由有三点:
第一,Paillier支持加法同态,投票计票本质上就是票数的累加,两者天然契合。第二,Python生态里有现成的phe(python-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 核心功能模块详解:登录、加密、计票三件套
登录模块我采用了JWT(JSON 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_length=2048): """ 生成Paillier公私钥对 :param n_length: 模数n的位数,默认2048位 :return: (公钥, 私钥) """ pub_key, priv_key = paillier.generate_paillier_keypair(n_length=n_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_path=None): 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方案已经能很好地体现同态加密的核心思想,没有必要一开始就上重型方案。
我最后再分享一个经验:做这类隐私保护系统,编码方案和密钥管理设计初期一定要先用文档定下来,不要急于写代码。我第一次就是直接上手,后来发现编码方案设计错了,数据库里的测试数据全废,只能重新设计重写。方案定了之后再动手,后面写代码和调试都能顺畅很多。希望这个项目拆解能帮到你,也祝你的投票系统早日跑通全流程。
本文还有配套的精品资源,点击获取