ARTICLE DETAIL

资讯详情

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

隐私优先预算应用:Python+SQLite+AES-GCM加密实现不触碰银行账户

隐私优先预算应用:Python+SQLite+AES-GCM加密实现不触碰银行账户 预算类应用的隐私问题往往不在记账动作本身而在于它有没有碰你的银行账户。很多预算工具为了自动同步交易记录会要求用户授权读取银行流水甚至把交易数据上传到云端。这种模式确实方便但带来一个关键风险预算工具一旦能触碰银行账户就同时承担了资金数据和隐私数据的双重责任而用户很难验证服务商的存储、审计和删除策略。另一种产品定位是 A budgeting app that cant touch your bank也就是从架构上隔离银行账户应用本身不具备读取资金数据的能力。这种“隐私优先Privacy by design”的思路正是本文要实现的预算应用的核心。这里要做的事情是用 Python 3 SQLite cryptography 实现一个本地优先的隐私预算应用不接入银行 API、不读取余额、不存储银行凭证用户通过手动记账或本地 CSV 导入记录流水敏感字段使用 AES-GCM 加密保存在本地并提供月度预算报告与数据导出。读者需要具备 Python 基础熟悉命令行和 SQLite 的基本概念。学完以后你可以把它扩展成桌面端或自托管 Web 应用也可以把同一个架构思路应用到其他本地优先工具。全文会按照“概念 - 环境 - 实现 - 验证 - 排错 - 优化”的顺序展开每一步都给出可复制、可验证的命令和代码。1. 为什么预算应用不应该触碰银行账户隐私设计的出发点1.1 常见预算应用的数据流和风险传统预算应用的典型数据流是这样的用户注册账号并授权应用连接银行账户。应用通过银行聚合接口、Open Banking 类服务或第三方数据服务持续同步交易流水和余额。服务端对交易进行分类、打标签形成报表和预算曲线。用户通过手机或网页查看分析结果。这条链路里应用不仅能看到每一笔消费金额还能看到商户名称、日期、余额变化甚至推断出用户的作息、消费习惯和家庭位置。更关键的是用户一旦完成授权应用就拥有了访问银行数据的长期能力。如果服务端数据库被拖库或者内部权限管控不严交易记录就可能批量泄露。隐私风险并不只存在于“恶意应用”还存在于“权限过大的正常应用”。1.2 “不能触碰银行账户”到底是什么意思“A budgeting app that cant touch your bank”并不是在功能上偷工减料而是从架构上做减法。它的核心特征很明确应用不接收任何银行登录凭证。应用不连接银行聚合 API也不读取余额。应用不要求用户填写银行卡号、网银密码或授权码。应用只处理用户主动提供的本地数据。换句话说银行账户对应用来说是不可达的。预算功能所需的交易数据由用户手动录入或者从银行导出的 CSV、OFX 等文件中本地导入。应用在导入后只负责解析、分类、加密和统计不产生任何向银行发起的网络请求。这种设计对应的正是隐私保护领域的几个基本原则。隐私设计原则在本应用中的落地方式数据最小化只收集本地预算所需的交易日期、金额、商户和分类不读取余额和账户权限本地优先数据默认保存在用户自己的设备上不强制上传云端用户控制用户决定录入什么、导入什么、导出什么、删除什么可迁移性数据可以用标准格式导出不绑定特定服务商透明可审计代码开源或可审查用户能确认应用没有连接银行的能力1.3 不连接银行账户后预算功能如何成立很多人会问不自动同步记账是不是很麻烦确实手动录入比自动同步要付出更多操作成本但预算应用的核心价值并不完全来自“自动”。预算的关键在于“控制”而不是“记录”。手动录入每一笔支出本身就是在增加对消费的感知这是很多自动同步应用缺失的环节。如果确实希望降低录入成本可以走另一条安全路径从银行导出 CSV 流水再在本地导入。整个过程中银行数据仍然由用户自己控制导出范围应用只接触一次导入数据不保留银行连接能力。对于预算分析来说CSV 中的日期、金额、交易描述、分类信息已经足够不需要额外的账户接口权限。1.4 两种方案对比对比维度连接银行 API 的预算应用不触碰银行账户的本地预算应用数据入口银行授权同步手动录入、CSV/OFX 本地导入权限范围通常包含账户余额和交易流水只处理用户主动提供的交易记录默认存储位置服务端云端本地设备转账/支付能力部分聚合接口支持发起交易风险更高应用没有银行凭证无法发起任何交易被拖库后的影响交易数据成批泄露本地数据库泄露字段仍需解密密钥用户掌控度取决于服务商删除和导出策略文件、密钥、数据都在用户手中2. 环境准备与项目结构先搭一个本地优先的基础2.1 技术选型与取舍本文选择 Python 3 SQLite cryptography 这套组合原因是尽量降低复现门槛。Python 3 自带 SQLite 模块不需要单独安装数据库服务cryptography 库提供成熟的 AES-GCM 加密实现避免自己编写密码学算法。整个应用只用命令行就能运行适合讲清隐私设计的核心链路。如果你在真实项目里使用 Node.js 或 Go思路仍然一致SQLite 或本地文件作为存储加密层放在数据读写入口密钥不落盘。技术栈可以换架构逻辑不需要变。还要说明一个取舍这里使用字段级加密而不是整个数据库加密。字段级加密有利于后续按日期和金额做统计查询因为日期和金额仍以明文形式参与 SQL 运算。敏感文本字段比如商户名称和备注则以密文保存。如果产品要求“数据库文件整体不可读”可以替换为 SQLCipher 或 SQLite 的加密扩展但需要注意查询性能和密钥管理方式的调整。2.2 环境要求项目要求操作系统Windows 10/11、macOS、Linux 均可Python3.10 及以上版本需要能运行 pip第三方库cryptography命令行能执行 python、pip 命令SQLitePython 自带无需安装在开始前先检查 Python 版本。python --version pip --version安装依赖pip install cryptography如果原始项目使用更严格的环境隔离推荐先创建虚拟环境再安装依赖。python -m venv venv source venv/bin/activate # Linux / macOS venv\Scripts\activate # Windows pip install cryptography2.3 项目目录结构最小可运行项目可以保持如下目录结构privacy_budget/ ├── main.py # 命令行入口 ├── crypto_utils.py # 密钥派生与 AES-GCM 加解密 ├── database.py # SQLite 初始化与数据操作 ├── csv_importer.py # CSV 导入解析 ├── requirements.txt # 依赖清单 ├── data/ # 运行时生成的数据库和盐文件 └── samples/ └── transactions.csv # 示例导入数据crypto_utils.py只负责加解密不接触业务逻辑database.py负责数据读写main.py负责把命令行参数映射到业务操作。这样分层以后后续增加 Web 界面或桌面界面时不需要改动加密模块。2.4 requirements.txt 与初始文件准备创建requirements.txtcryptography41.0.0创建空目录mkdir -p privacy_budget/data privacy_budget/samples cd privacy_budget这一步没有技术难度但建议把数据库、盐文件和 CSV 示例分目录存放避免后续误删。3. 核心实现敏感字段加密预算功能全部本地完成3.1 数据模型设计隐私预算应用的表结构不需要复杂核心是分类表和交易表。分类表用于记录“餐饮”“交通”“工资”等项交易表用于记录每一笔流水。CREATE TABLE IF NOT EXISTS categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, monthly_budget REAL NOT NULL DEFAULT 0, sort_order INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, tx_date TEXT NOT NULL, amount REAL NOT NULL, category_id INTEGER NOT NULL, merchant_enc TEXT NOT NULL, notes_enc TEXT NOT NULL DEFAULT , created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES categories(id) );这里的merchant_enc和notes_enc存储的是加密后的文本而不是明文。tx_date使用YYYY-MM-DD字符串方便 SQL 按月份聚合。amount保留正数表示支出、负数表示收入这样月度统计可以直接用SUM(amount)判断支出总额。要强调的是不要把加密逻辑和业务查询混在一起。业务查询依赖日期和金额所以这两个字段保存明文而商户和备注属于用户隐私信息保存密文。如果项目要求所有字段都不可见就需要在查询后解密整个结果集数据量小的时候可以接受数据量大时要提前设计索引和分页。3.2 密钥派生与 AES-GCM 加密工具隐私设计的关键是密钥管理。这里不把密钥硬编码在代码里也不直接把密钥写进数据库而是由用户输入口令通过 PBKDF2 派生加密密钥。首次初始化时生成随机盐之后每次运行都要求用户输入同一个口令。新建crypto_utils.pyimport base64 import os from cryptography.hazmat.primitives import hashes from cryptography.hazmat.primitives.kdf.pbkdf2 import PBKDF2HMAC from cryptography.hazmat.primitives.ciphers.aead import AESGCM def derive_key(password: str, salt: bytes, iterations: int 200_000) - bytes: kdf PBKDF2HMAC( algorithmhashes.SHA256(), length32, saltsalt, iterationsiterations, ) return kdf.derive(password.encode(utf-8)) def encrypt_text(key: bytes, plaintext: str) - str: aesgcm AESGCM(key) nonce os.urandom(12) ciphertext aesgcm.encrypt(nonce, plaintext.encode(utf-8), None) return base64.b64encode(nonce ciphertext).decode(ascii) def decrypt_text(key: bytes, token: str) - str: data base64.b64decode(token) nonce, ciphertext data[:12], data[12:] aesgcm AESGCM(key) plaintext aesgcm.decrypt(nonce, ciphertext, None) return plaintext.decode(utf-8)关键点有三个。第一PBKDF2HMAC用用户口令加随机盐生成密钥。迭代次数默认 200000不要为了性能调到几千这个参数就是用来抵抗暴力破解的。第二AES-GCM 是认证加密模式解密时如果密钥错误或密文被篡改会直接抛出异常。这比普通 CBC 模式更安全因为应用能发现“解密结果不可信”。第三nonce是随机生成的 12 字节随机数拼接在密文前一起做 Base64 编码。每次加密都必须使用新的 nonce这里用os.urandom(12)生成满足要求。3.3 数据库操作与手动记账新建database.py。这个文件负责创建表、保存数据、查询数据和输出报告。import os import sqlite3 from crypto_utils import decrypt_text, encrypt_text def connect_db(db_path: str) - sqlite3.Connection: conn sqlite3.connect(db_path) conn.row_factory sqlite3.Row return conn def init_db(conn: sqlite3.Connection) - None: conn.executescript( CREATE TABLE IF NOT EXISTS categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, monthly_budget REAL NOT NULL DEFAULT 0, sort_order INTEGER NOT NULL DEFAULT 0 ); CREATE TABLE IF NOT EXISTS transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, tx_date TEXT NOT NULL, amount REAL NOT NULL, category_id INTEGER NOT NULL, merchant_enc TEXT NOT NULL, notes_enc TEXT NOT NULL DEFAULT , created_at TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, FOREIGN KEY (category_id) REFERENCES categories(id) ); ) def get_or_create_category(conn: sqlite3.Connection, name: str, monthly_budget: float 0) - int: row conn.execute(SELECT id FROM categories WHERE name ?, (name,)).fetchone() if row: return row[id] cur conn.execute( INSERT INTO categories (name, monthly_budget, sort_order) VALUES (?, ?, 0), (name, monthly_budget), ) return cur.lastrowid def add_transaction( conn: sqlite3.Connection, key: bytes, tx_date: str, amount: float, category_name: str, merchant: str, notes: str , monthly_budget: float 0, ) - int: category_id get_or_create_category(conn, category_name, monthly_budget) merchant_enc encrypt_text(key, merchant) notes_enc encrypt_text(key, notes) cur conn.execute( INSERT INTO transactions (tx_date, amount, category_id, merchant_enc, notes_enc) VALUES (?, ?, ?, ?, ?) , (tx_date, amount, category_id, merchant_enc, notes_enc), ) conn.commit() return cur.lastrowid def list_transactions(conn: sqlite3.Connection, key: bytes) - list: rows conn.execute( SELECT t.id, t.tx_date, t.amount, c.name AS category, t.merchant_enc, t.notes_enc FROM transactions t JOIN categories c ON t.category_id c.id ORDER BY t.tx_date DESC ).fetchall() result [] for row in rows: result.append( { id: row[id], tx_date: row[tx_date], amount: row[amount], category: row[category], merchant: decrypt_text(key, row[merchant_enc]), notes: decrypt_text(key, row[notes_enc]), } ) return result def monthly_report(conn: sqlite3.Connection, key: bytes, month_prefix: str) - list: rows conn.execute( SELECT c.name AS category, c.monthly_budget AS budget, SUM(t.amount) AS spent FROM transactions t JOIN categories c ON t.category_id c.id WHERE t.tx_date LIKE ? GROUP BY c.id ORDER BY spent DESC , (month_prefix %,), ).fetchall() report [] for row in rows: report.append( { category: row[category], budget: row[budget] or 0, spent: row[spent] or 0, } ) return report这里要注意几个细节。get_or_create_category的monthly_budget参数只在第一次创建分类时生效后续新增交易不会覆盖已有预算。如果你希望每次导入都能设置预算需要单独提供set-budget命令。list_transactions返回的是解密后的明文因此调用方必须持有正确的密钥。如果口令输入错误解密时会抛异常这正好能在用户端提前暴露问题而不是把所有交易都显示成乱码。3.4 CSV 导入作为“银行同步”的安全替代方案用户从银行网页端或 App 导出 CSV 后应用在本地解析导入。这是一个典型的批处理流程。新建csv_importer.pyimport csv import sys from database import add_transaction def import_csv(conn, key, file_path: str, category_map: dict | None None): category_map category_map or {} with open(file_path, r, encodingutf-8-sig, newline) as f: reader csv.DictReader(f) count 0 for row in reader: date row.get(date) or row.get(日期) amount row.get(amount) or row.get(金额) merchant row.get(merchant) or row.get(商户) raw_category row.get(category) or row.get(分类) or 未分类 notes row.get(notes) or row.get(备注) or if not date or not amount: continue # 通过 mapping 将银行原始分类映射为本地分类 category_name category_map.get(raw_category, raw_category) amount float(amount) if amount 0: # 代表支出直接按正数存储 pass else: # 银行导出可能用负数表示收入这里保持原样 pass add_transaction( conn, key, date, amount, category_name, merchant, notes ) count 1 return countCSV 字段建议按下面的最小格式准备字段含义示例date交易日期YYYY-MM-DD2025-01-05amount金额支出正数收入负数-32.50merchant商户名称某连锁超市category分类名称餐饮notes备注午餐导入时使用utf-8-sig编码读取文件可以同时兼容在 Windows 上导出的带 BOM 的 UTF-8 文件避免第一列列名变成\ufeffdate。这是本地 CSV 导入最常见的坑之一。3.5 命令行入口与示例 CSV 文件新建main.py把初始化、增删查、导入和报告统一到一个命令行入口中。import os import sqlite3 import argparse import getpass from crypto_utils import derive_key, encrypt_text, decrypt_text from database import ( init_db, connect_db, add_transaction, list_transactions, monthly_report, ) from csv_importer import import_csv def prompt_key(salt_path: str, iterations: int 200_000) - bytes: password getpass.getpass(请输入解密口令: ) with open(salt_path, rb) as f: salt f.read() return derive_key(password, salt, iterationsiterations) def main(): parser argparse.ArgumentParser(descriptionPrivacy-first budgeting app) subparsers parser.add_subparsers(destcommand) subparsers.add_parser(init, help初始化数据库与盐文件) subparsers.add_parser(list, help列出所有交易) subparsers.add_parser(report, help生成月度报告) parser_add subparsers.add_parser(add, help添加一笔交易) parser_add.add_argument(--date, requiredTrue) parser_add.add_argument(--amount, requiredTrue, typefloat) parser_add.add_argument(--category, requiredTrue) parser_add.add_argument(--merchant, requiredTrue) parser_add.add_argument(--notes, default) parser_csv subparsers.add_parser(import-csv, help导入 CSV) parser_csv.add_argument(--file, requiredTrue) args parser.parse_args() data_dir os.path.join(os.path.dirname(__file__), data) db_path os.path.join(data_dir, budget.db) salt_path os.path.join(data_dir, key.salt) if args.command init: os.makedirs(data_dir, exist_okTrue) # 生成随机盐 salt os.urandom(16) with open(salt_path, wb) as f: f.write(salt) # 输入并确认口令 password getpass.getpass(设置解密口令: ) password2 getpass.getpass(再次输入解密口令: ) if password ! password2: print(两次口令不一致) return conn connect_db(db_path) init_db(conn) conn.close() print(初始化完成。数据库:, db_path) return if not os.path.exists(salt_path) or not os.path.exists(db_path): print(请先运行 python main.py init) return key prompt_key(salt_path) conn connect_db(db_path) if args.command add: add_transaction( conn, key, args.date, args.amount, args.category, args.merchant, args.notes, ) print(交易已添加) elif args.command list: for item in list_transactions(conn, key): print(item) elif args.command report: month input(请输入月份(YYYY-MM): ).strip() report monthly_report(conn, key, month) for row in report: status OK if row[spent] row[budget] else OVER print( f{row[category]}: 预算 {row[budget]:.2f}, 已支出 {row[spent]:.2f} [{status}] ) elif args.command import-csv: count import_csv(conn, key, args.file) print(f已导入 {count} 条记录) conn.close() if __name__ __main__: main()这里的getpass不会在终端回显口令避免肩窥和日志泄露。init子命令会生成新的盐文件并初始化空数据库这个流程只执行一次。在samples/transactions.csv中准备一个简单示例date,amount,merchant,category,notes 2025-01-01,-89.00,某超市,生活用品,月度采购 2025-01-02,-23.50,某咖啡店,餐饮,商务咖啡 2025-01-03,5000.00,工资,工资,一月工资 2025-01-04,-45.00,地铁,交通,通勤4. 运行验证从初始化到加密存储检查4.1 初始化数据库并设置密钥python main.py init会在data/目录下生成两个文件budget.db和key.salt。key.salt里保存的是随机盐不是密钥本身。真正用于加解密的是“盐 你输入的口令”派生出来的 32 字节密钥。输入两次口令后终端输出初始化完成。数据库: /your/path/privacy_budget/data/budget.db如果提示模块不存在先执行pip install cryptography再重新运行。4.2 添加手动交易python main.py add --date 2025-01-05 --amount -32.50 --category 餐饮 --merchant 某外卖平台 --notes 晚餐这里amount用负数表示支出。-32.50会被存储到数据库但是商户名称和备注会以密文保存。执行后输出交易已添加再跑一次list查看解密后的数据python main.py list预期会看到交易列表中包含刚才添加的记录包括日期、金额、分类、商户和备注。4.3 导入示例 CSV使用上一步生成的示例 CSV 文件python main.py import-csv --file samples/transactions.csv如果 CSV 里包含 4 条记录会输出已导入 4 条记录导入成功后用list确认共有 5 条记录。注意 CSV 中的-89.00、-23.50等负数都会被视为支出收入记录5000.00保持正数。4.4 生成月度预算报告python main.py report输入2025-01后输出类似工资: 预算 0.00, 已支出 5000.00 [OVER] 生活用品: 预算 0.00, 已支出 89.00 [OVER] 餐饮: 预算 0.00, 已支出 56.00 [OVER] 交通: 预算 0.00, 已支出 45.00 [OVER]这里的“预算 0.00”是因为初始化时没有给分类设置预算。在实际使用中你应该在导入前给主要分类设置月度预算例如餐饮 1500、生活用品 800、交通 500。文章末尾会给出设置预算的扩展建议。4.5 验证数据库中看不到明文这是隐私设计最需要的一个验证步骤确认密文确实生效。打开 SQLite 数据库查看表内容。python -c import sqlite3; connsqlite3.connect(data/budget.db); print(conn.execute(select merchant_enc, notes_enc from transactions limit 3).fetchall())预期输出是类似这样的 Base64 密文片段[(9/vk2...A, 6o0c...A), ...]你看不到“某外卖平台”这类明文。这说明即使数据库文件被拷贝走没有解密口令和盐文件攻击者也无法还原商户名称和备注。如果还想确认日期和金额保持明文可以查询tx_date和amount字段python -c import sqlite3; connsqlite3.connect(data/budget.db); print(conn.execute(select tx_date, amount from transactions limit 3).fetchall())这会输出[(2025-01-05, -32.5), ...]日期和金额明文是为了支持按月份和分类做 SQL 聚合这是字段级加密的取舍。4.6 验证口令错误时的表现输入错误口令运行python main.py list会看到类似异常cryptography.exceptions.InvalidTag这在 AES-GCM 认证加密中是正常现象表示解密失败。应用应该捕获这个异常并提示用户重新输入口令而不是继续显示乱码。生产版本中建议在main.py的入口增加try/except把异常包装成更友好的中文提示。5. 常见问题排查与隐私设计清单5.1 高频问题对照表问题现象常见原因检查方式处理建议导入后记录变空白CSV 文件名不是 UTF-8或列名不匹配打印 CSV 第一行列名使用 utf-8-sig 读取统一 date/amount/merchant 列名解密时报 InvalidTag输入口令错误或 key.salt 与数据库不匹配检查盐文件是否备份一致重新输入口令保持 key.salt 与 budget.db 一起备份只有日期金额没有商户备注字段级加密设计本身如此用 list 命令查看解密结果在 UI 层统一使用加解密方法不直接读取加密字段CSV 首列出现\ufeffCSV 带 BOMPython 默认按 UTF-8 解析打开文件看第一个字符使用 encodingutf-8-sig收入被当成支出金额符号规则不统一检查 CSV 中收入是正数还是负数在导入前约定支出为负收入为正并在文档中说明预算花费正确但排序不对数据库默认按支出排序查询 SQL 的 ORDER BY根据产品需要调整为按预算或类别排序5.2 排查链路从“数据没了”到“重新导入”遇到“数据不见”或“导入没生效”时可以按下面顺序排查。先确认数据库文件是否存在体积是否变化。用sqlite3打开库看transactions表是否有行。如果表里有密文但程序查不到检查口令和盐文件。出现InvalidTag说明能加载数据库但无法解密。如果导入端口提示 0 条查看 CSV 前几行列名确认是不是大小写不同。如果导入时金额全成了 0检查 CSV 里金额列是不是带千分位分隔符例如1,234.00会被float()解析失败。如果报告月份为空确认日期格式是YYYY-MM-DD而不是MM/DD/YYYY。这类排查顺序的核心是“先看存储再看解析最后看加密层”。不要一上来就怀疑加密代码大多数问题出在 CSV 字段和日期格式。5.3 隐私设计检查清单在把本地预算应用交付给真实用户前建议先过一遍清单。[ ] 数据库文件权限设置为当前用户可读写例如 Linux 下执行chmod 600 data/budget.db data/key.salt。[ ] 用户口令不落盘、不写日志、不写入数据库。[ ] 日志中不打印解密后的商户和备注。[ ] 应用不发起任何到银行域名的网络请求。[ ] 导出文件使用标准格式用户可以随时迁移。[ ] 删除记录时同步清理对应密文避免残留。[ ] 备份时同时备份数据库和盐文件缺一不可。[ ] 源码库中不提交任何*.db、*.salt和真实 CSV 数据。5.4 学习环境与生产环境的差异在本地命令行演示中一个 Python 进程直接持有密钥数据库也在同一台机器上这已经能满足个人隐私需求。但进入生产环境后还需要补上几层保障。场景补充工作桌面应用使用系统钥匙串存储口令或派生密钥而不是每次手工输入自托管 Web 应用增加 TLS、用户认证、访问控制服务端不保存银行凭证多人团队使用增加角色权限审计日志里不记录明文交易高隐私需求考虑全库加密 SQLCipher或对金额也做加密后只在内存中计算数据恢复提供助记词、恢复码或密钥分片方案避免盐文件丢失后无法解密这里最需要提醒的是隐私设计并不等于“不用数据库”而是“把密钥、权限和数据最小化这三件事做好”。SQLite 文件本身可以放在移动设备或电脑本地真正决定数据是否安全的是密钥管理和文件权限。6. 扩展方向让隐私预算应用更完整6.1 数据导出与迁移隐私设计要支持用户随时离开。新增一个export子命令把数据库中的交易解密后导出为 JSON 或 CSV就可以实现数据迁移。python main.py export --format csv --file backup.csv建议导出时保留字段date, amount, category, merchant, notes这样其他记账工具也能导入。更重要的是导出操作必须使用解密密钥不能直接复制数据库文件否则备份中会包含加密字段迁移到新环境后会无法阅读。6.2 本地 Web 界面与桌面封装命令行工具适合开发和验证普通用户更需要可视化界面。最简单的扩展是使用 Flask 启动一个只监听127.0.0.1的本地 Web 服务浏览器访问http://127.0.0.1:8000来完成记账和查看报告。这个服务不应该监听公网 IP否则就打破了“本地优先”的边界。另一个方向是使用 PySide、Tauri 或 Electron 封装成桌面应用。桌面应用可以把密钥交给系统钥匙串管理用户体验和安全性都能提升。6.3 可选云同步先做端到端加密如果有多设备需求可以考虑云同步但第一步不是把 SQLite 文件直接上传而是先做端到端加密。用户使用主密钥加密整个数据库或导出包再把加密包放到 WebDAV、S3 或其他对象存储中。原理与当前字段级加密类似只是加密粒度从单字段变成了整个同步文件。这里要谨慎控制复杂度。云同步涉及密钥恢复、版本冲突、回收站、丢密钥后的数据恢复等问题。在最小预算应用中建议优先保持“纯本地”等用户明确需要多设备同步时再引入经过设计验收的同步方案。6.4 开源与第三方审计“Privacy by design”不只是技术架构也是可持续验证的产品承诺。如果项目是开源的用户可以查看代码确认应用确实没有请求银行域名的逻辑也可以提交 issue 报告数据导出或权限问题。如果有条件可以请第三方安全审计团队评估密钥管理、导入解析、存储加密和日志输出。对个人开发者来说最实际的审计步骤是自己在部署环境里执行一遍“数据库不可见明文、日志不可见明文、网络请求不可触碰银行”三项检查并把检查方法写进项目 README。这样用户不需要盲信产品承诺而是能自己验证。回到开头的问题预算应用要不要连接银行账户在自动同步和便捷汇总面前这个选择很容易被动摇。但从隐私设计角度看“不触碰银行账户”不是功能缺陷而是把数据控制权交还给用户的一种明确态度。本文实现的最小应用用 SQLite 存储、AES-GCM 字段级加密和 CSV 本地导入替换了银行连接数据始终留在用户自己的设备上密钥由用户口令派生。你可以在它的基础上增加 Web 界面、端到端云同步、系统钥匙串和多维度统计但核心的隐私边界不需要改变应用不拥有银行凭证也就不承担银行级权限风险。对于刚开始做隐私工具的人来说这个边界值得先守住。
返回列表