Python中如何非明文存储字符串键+字节值的字典到文件?
问题描述
我正在开发一款可在Windows、iOS、Android平台运行的Python程序,用于处理登录功能,使用格式为{key_string: hashed_password_bytes}的dict存储凭据。目前我手动将字符串转换为二进制并拼接成二进制串,读取时再进行解析,文件I/O操作虽简单但实现代码较为冗长。
尝试过多个工具包但均未找到合适的使用方式:
- Pickle:因存在代码注入风险被弃用
- YAML、JSON等:属于明文方案,不适用于存储凭据
- Struct:需要提前知晓变量及字符串的长度
- Netstruct:支持可变长字符串,但无法实现字典的解包,也不清楚如何定义格式字符串来处理未知数量的字符串与字节混合变量
- Pyvault:可简化操作,但依赖sqlcipher,不愿在所有运行设备上安装该依赖
请问是否有可实现非明文存储字典的工具包来替代当前方案?
当前实现代码(调试用的print语句后续会移除):
import scrypt import os MAX_TIME = 0.5 # Set here for ease of changing FILE_001_DAT_LOCATION = "Source/001.dat" DATA_LENGTH = 64 # Data length for hashing password ENCODING_STRING = 'utf-8' # Used in all string encode and decode PACKING_SIZE_STRING_LENGTH = 4 # The number of bytes used by the size indicator for the proceeding username/password SIZE_STRING_FORMAT = '{:0>4}' # Used to format the size integer for packing dictionaries # Turns {string: bytes} into bytes for writing to file # Format is username1size, username1, password1size, password1, username2size... def pack_credentials(credentials: dict): bytes_out = b"" print("pack_credentials() called. Starting loop") # Loop over every key and add keys for key in credentials.keys(): # Pack username print(f"Encoding username: {key}") encoded_username = key.encode(ENCODING_STRING) username_size = len(encoded_username) print(f"Encoded username has size: {username_size}") username_size_string = SIZE_STRING_FORMAT.format(username_size) bytes_out += username_size_string.encode(ENCODING_STRING) + encoded_username # Pack hashed password print(f"Encoding password: {str(credentials[key])}") password_size = len(credentials[key]) print(f"Hashed password has size: {password_size}") password_size_string = SIZE_STRING_FORMAT.format(password_size) bytes_out += password_size_string.encode(ENCODING_STRING) + credentials[key] print("Packing complete. Returning bytes.") return bytes_out # Hash a raw password and return - for adding new credentials to the dictionary def hash_password(password): return scrypt.encrypt(os.urandom(DATA_LENGTH), password, maxtime=MAX_TIME) # Turns {string: bytes} into bytes for writing to file # Format is username1size, username1, password1size, password1, username2size... def unpack_credentials(bytes_in: bytes): credentials_out = {} # Defined here to add to in loop slice_start = 0 # Starting index for next slice from bytes_in slice_end = slice_start + PACKING_SIZE_STRING_LENGTH # Ending index for next slice from bytes_in bytes_in_length = len(bytes_in) print("unpack_credentials() called. Starting loop") # Loop over every key and add keys while slice_end < bytes_in_length: # Unpack username print(f"Unpacking username size") username_length_bytes = bytes_in[slice_start:slice_end] print(f"Username size: {username_length_bytes}") print(f"Unpacking username") slice_start = slice_end slice_end = slice_end + int(username_length_bytes.decode(ENCODING_STRING)) username_bytes = bytes_in[slice_start:slice_end] print(f"Username: {username_bytes.decode(ENCODING_STRING)}") # Pack hashed password print(f"Unpacking password size") slice_start = slice_end slice_end = slice_end + PACKING_SIZE_STRING_LENGTH password_length_bytes = bytes_in[slice_start:slice_end] print(f"Password size: {password_length_bytes.decode(ENCODING_STRING)}") slice_start = slice_end slice_end = slice_end + int(password_length_bytes.decode(ENCODING_STRING)) print(f"Unpacking password") password_bytes = bytes_in[slice_start:slice_end] #Prepare for next iteration slice_start = slice_end slice_end = slice_end + PACKING_SIZE_STRING_LENGTH #Populate dict credentials_out[username_bytes.decode(ENCODING_STRING)] = password_bytes print("Unpacking complete. Returning dict.") return credentials_out if __name__ == '__main__': # appGUI = MyApp() # appGUI.run() original_credentials = {"User 1 boiiiiiii": hash_password("password1wEDFRAwerwq rq"), "Sonic the hedgehog": hash_password("password2qwergasdfghwerterwqgdfsg"), "Misty water colour memories": hash_password("password3qdaserfgwqeryhger5wtywrthsfdghsd"), "Niko the big black dog": hash_password("password4eadgrasewrdgerwqttgeqrwtgdfgewrtyertertgeertet")} credentials_binary = pack_credentials(original_credentials) new_credentials = unpack_credentials(credentials_binary) print( f"Do password hashes match? {original_credentials['User 1 boiiiiiii'] == new_credentials['User 1 boiiiiiii']}")
解决方案
推荐以下几个轻量、跨平台且满足非明文存储需求的方案:
1. Msgpack + Fernet加密(推荐)
Msgpack是一种高效的二进制序列化格式,原生支持字符串和字节类型,无需提前定义长度;配合cryptography库的Fernet模块,可以轻松实现加密存储,且依赖简单,跨平台兼容性好。
实现代码
import scrypt import os import msgpack from cryptography.fernet import Fernet MAX_TIME = 0.5 FILE_001_DAT_LOCATION = "Source/001.dat" DATA_LENGTH = 64 # 首次运行生成密钥,之后要保存好(比如存在用户配置目录,不要硬编码) # key = Fernet.generate_key() # with open("key.key", "wb") as f: # f.write(key) key = b"your-generated-fernet-key-here" fernet = Fernet(key) def hash_password(password): return scrypt.encrypt(os.urandom(DATA_LENGTH), password, maxtime=MAX_TIME) def save_credentials(credentials: dict): # 将字典序列化为msgpack二进制 packed = msgpack.packb(credentials, use_bin_type=True) # 加密后写入文件 encrypted = fernet.encrypt(packed) with open(FILE_001_DAT_LOCATION, "wb") as f: f.write(encrypted) def load_credentials() -> dict: with open(FILE_001_DAT_LOCATION, "rb") as f: encrypted = f.read() # 解密后反序列化为字典 packed = fernet.decrypt(encrypted) return msgpack.unpackb(packed, raw=False) if __name__ == '__main__': original_credentials = { "User 1 boiiiiiii": hash_password("password1wEDFRAwerwq rq"), "Sonic the hedgehog": hash_password("password2qwergasdfghwerterwqgdfsg"), "Misty water colour memories": hash_password("password3qdaserfgwqeryhger5wtywrthsfdghsd"), "Niko the big black dog": hash_password("password4eadgrasewrdgerwqttgeqrwtgdfgewrtyertertgeertet") } save_credentials(original_credentials) loaded_credentials = load_credentials() print(f"Do password hashes match? {original_credentials['User 1 boiiiiiii'] == loaded_credentials['User 1 boiiiiiii']}")
优势
- 代码简洁,完全替代手动的pack/unpack逻辑
- Msgpack原生支持字符串和字节,无需处理长度编码
- Fernet加密安全可靠,自动处理密钥管理、签名验证
- 依赖
msgpack和cryptography,在Windows/iOS/Android的Python环境(如Kivy、Pyto、Termux)中均可安装
2. 使用Shelve + 文件加密包装
Shelve是Python标准库中的轻量持久化工具,提供类似字典的接口,无需手动序列化。可以通过在文件读写层增加加密来实现非明文存储。
实现代码
import scrypt import os import msgpack import shelve from cryptography.fernet import Fernet MAX_TIME = 0.5 FILE_001_DAT_LOCATION = "Source/001" # Shelve会生成多个关联文件 DATA_LENGTH = 64 key = b"your-generated-fernet-key-here" fernet = Fernet(key) def hash_password(password): return scrypt.encrypt(os.urandom(DATA_LENGTH), password, maxtime=MAX_TIME) def save_credentials(credentials: dict): # 先序列化字典为字节,加密后写入shelve with shelve.open(FILE_001_DAT_LOCATION, 'n') as db: packed = msgpack.packb(credentials, use_bin_type=True) db['credentials'] = fernet.encrypt(packed) def load_credentials() -> dict: with shelve.open(FILE_001_DAT_LOCATION, 'r') as db: encrypted = db['credentials'] packed = fernet.decrypt(encrypted) return msgpack.unpackb(packed, raw=False)
优势
- 基于标准库,无需额外安装(除了加密依赖)
- 接口类似字典,使用简单
- 适合需要频繁修改单个凭据的场景
3. PySodium(libsodium绑定)
libsodium是一个轻量、安全的加密库,PySodium是其Python绑定。可以用它的crypto_secretbox来加密序列化后的字典,安全性极高,且跨平台支持完善。
实现代码
import scrypt import os import msgpack import pysodium MAX_TIME = 0.5 FILE_001_DAT_LOCATION = "Source/001.dat" DATA_LENGTH = 64 # 生成密钥,之后保存好(不要硬编码) # key = pysodium.crypto_secretbox_keygen() # with open("key.key", "wb") as f: # f.write(key) key = b"your-generated-libsodium-key-here" def hash_password(password): return scrypt.encrypt(os.urandom(DATA_LENGTH), password, maxtime=MAX_TIME) def save_credentials(credentials: dict): packed = msgpack.packb(credentials, use_bin_type=True) nonce = pysodium.randombytes(pysodium.crypto_secretbox_NONCEBYTES) encrypted = pysodium.crypto_secretbox(packed, nonce, key) # 非ce和密文一起写入文件 with open(FILE_001_DAT_LOCATION, "wb") as f: f.write(nonce + encrypted) def load_credentials() -> dict: with open(FILE_001_DAT_LOCATION, "rb") as f: data = f.read() nonce = data[:pysodium.crypto_secretbox_NONCEBYTES] encrypted = data[pysodium.crypto_secretbox_NONCEBYTES:] packed = pysodium.crypto_secretbox_open(encrypted, nonce, key) return msgpack.unpackb(packed, raw=False)
优势
- 加密算法安全性极高,适合敏感凭据存储
- libsodium跨平台支持好,体积小
- 无需担心序列化格式的安全问题
总结
优先选择Msgpack + Fernet方案,兼顾代码简洁性、安全性和跨平台兼容性,完全替代你当前手动实现的二进制打包逻辑,且避免了Pickle的安全风险和JSON/YAML的明文问题。
内容的提问来源于stack exchange,提问作者Flash_Steel
相关产品推荐
相关产品推荐

