资讯动态

Qt实现AES-CBC加解密:配置文件加密工具开发实战

发布时间:2026/10/21 18:55:01 来源:尧图企业网站定制
简介一套面向Qt开发者的高级加密标准AES加密解密示例工程解决Qt框架未内置对称加密、需借助第三方密码学库的问题。压缩包内共5个文件整体大小约10KB包括AES加密类的头文件与实现文件AES.h与AES.cpp、调用演示主程序main.cpp以及Qt工程文件与工程用户配置.pro和.pro.user可直接打开工程参考CryptoC密码学库的集成方式来运行。AES作为广泛使用的对称块密码算法支持128位数据块以及128/192/256位密钥长度兼顾高安全性与较快的加解密速度。示例代码完整演示了密钥与初始化向量的准备、密码块链接CBC模式下加密器和解密器的创建、流式数据过滤及内存管理清晰还原可运行的加解密全流程适合需要为桌面、移动或嵌入式应用增加敏感数据保护的中初级Qt开发者。目前已有1477人浏览学习包体小巧而实用可在该示例基础上继续扩展随机密钥生成、文件分块读写、安全存储与异常处理作为正式项目内的加密模块参考。1. 用 Qt 实现 AES 加解密一个配置加密工具的开发起点某位同事把一个桌面客户端的配置文件丢给我数据库口令、接口 Token 全明文躺在 ini 文件里当时我第一个念头就是用 Qt 开发语言C封装一个本地的 AES 加解密模块。这个项目就是那个方案的完整落地它提供一个不依赖界面的AesEncryptor核心类支持 AES-128/192/256-CBC 的文本与文件加解密界面层用 Qt Widgets 搭出明文、密文、口令输入区加密结果以 Base64 输出解密结果能原样回填。适合正在做本地数据加密的 Qt 开发者无论是给配置文件、数据库密码还是接口凭据做保护都能直接把这套逻辑拖进自己的工程。最值得看的不只是加解密本身而是 QString、QByteArray 和 OpenSSL C 接口这三层数据在编码、IV、填充这些边界上怎么搬运——这些点踩明白了后面换 GCM、换文件加密都不慌。2. 技术选型与工程结构为什么选 AES-CBC 和 OpenSSL EVP 接口2.1 加密方案选型AES-CBC 与 PKCS7 填充、Base64 外壳AES 是对称分组加密算法密钥长度只有 128、192、256 三档对应的字节数是 16、24、32。Qt 自带的 QCryptographicHash 只做哈希不做分组加密所以 AES 这一步必须借助 OpenSSL 或第三方库。这个项目选 OpenSSL 的 EVP 接口理由是它把算法细节收敛成一个初始化函数加两个 update/final 调用换算法只改一行 cipher 参数底层还能吃到 AES-NI 硬件加速性能不用担心。分组加密有模式问题。ECB 模式下相同的明文块会得到相同的密文块加密一张有规律的图片时密文仍然能看出原图轮廓所以能不用就别用。项目选 CBC 模式每个明文块先与上一个密文块异或再加密相同明文块在不同位置得到不同密文再配合随机 IV同一个文本每次加密结果都不一样这是本地加密的基本要求。GCM 模式多了一层认证能力适合网络报文但落地文件用 CBC 更直观核心代码也更好讲第 6 章会给 GCM 的升级路径。IV 是 CBC 模式的必要参数长度固定 16 字节。这个项目的做法是每次加密用RAND_bytes生成随机 IV拼在密文头部一起输出解密时先取前 16 字节再解后面的密文。这样文件自包含不需要单独存储 IV复制到任何机器都能解。最后的 Base64 编码纯粹是为了让密文能在 JSON、INI、数据库里安全存放避免二进制字节被截断和转义。2.2 工程目录与构建配置CMake 组织 Qt Widgets OpenSSL工程结构是标准的 Qt Widgets 应用加解密逻辑独立成一个类界面和算法解耦QtAesDemo/ ├── CMakeLists.txt ├── main.cpp ├── MainWindow.h ├── MainWindow.cpp ├── AesEncryptor.h ├── AesEncryptor.cpp └── resources/AesEncryptor不依赖 QWidget可以直接被后台任务、命令行工具或单元测试复用。MainWindow只做一件事把界面上的 QString 转成 QByteArray然后调用AesEncryptor。CMakeLists.txt 这样组织cmake_minimum_required(VERSION 3.16) project(QtAesDemo VERSION 1.0 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_AUTOMOC ON) set(CMAKE_AUTORCC ON) find_package(Qt6 COMPONENTS Widgets REQUIRED) find_package(OpenSSL REQUIRED) add_executable(QtAesDemo main.cpp MainWindow.cpp MainWindow.h AesEncryptor.cpp AesEncryptor.h ) target_link_libraries(QtAesDemo PRIVATE Qt6::Widgets OpenSSL::Crypto )CMAKE_AUTOMOC让 Qt 的元对象编译器自动处理信号槽不用手工跑 moc。链接OpenSSL::Crypto而不是OpenSSL::SSL因为这里只需要 libcrypto 里的 EVP 和RAND_bytes不需要 TLS 协议那套。如果 CMake 找不到 OpenSSL在配置命令里加-DOPENSSL_ROOT_DIR/path/to/openssl指向安装目录即可Windows 下 MinGW 的链接坑在第 5 章单独说。2.3 核心类接口设计AesEncryptor 的公开接口AesEncryptor的类声明很简洁#pragma once #include QByteArray class AesEncryptor { public: enum class KeyLength { Aes128 16, Aes192 24, Aes256 32 }; explicit AesEncryptor(KeyLength keyLength KeyLength::Aes256); QByteArray encrypt(const QByteArray plainData, const QByteArray passphrase) const; QByteArray decrypt(const QByteArray cipherData, const QByteArray passphrase, bool* ok nullptr) const; private: QByteArray deriveKey(const QByteArray passphrase) const; KeyLength m_keyLength; };这里最需要注意的设计是入参和返回值都用QByteArray而不是 QString。原因是 AES 加密的是字节序列QString 内部是 UTF-16直接拿 QString 的底层指针去加密会连编码头一起处理解密回来再转 QString 很容易乱。把 QString 到 QByteArray 的转换留在 MainWindow 层界面统一按 UTF-8 转核心类就不需要关心编码。KeyLength枚举值直接就是字节数这个设计在映射EVP_aes_128_cbc/EVP_aes_192_cbc/EVP_aes_256_cbc时非常方便枚举值转 int 就能做 switch 条件。deriveKey是私有函数负责把用户口令展开成固定长度的密钥正常这个位置应该放 PBKDF2Demo 里先做简化第 6 章再补生产实现。ok指针用来把解密失败原因口令错误、密文被篡改、填充异常透出给界面层C 风格的输出参数比抛异常更适合“失败是常态”的解密场景。3. 核心模块拆解AES 初始化的密钥派生与加解密完整链路3.1 密钥派生与 IV 处理从口令到 32 字节密钥用户输入的口令长度是任意的可能 6 位也可能 20 位。AES 不接受这种输入它认的是固定长度密钥。deriveKey做的事情是把口令哈希后截取需要的长度QByteArray AesEncryptor::deriveKey(const QByteArray passphrase) const { QByteArray digest QCryptographicHash::hash( passphrase, QCryptographicHash::Sha256); return digest.left(static_castint(m_keyLength)); }逻辑说明先对整个口令做 SHA-256得到 32 字节摘要再按m_keyLength截取前 16/24/32 字节。这样不论用户输入多长的口令密钥长度永远正确。SHA-256 摘要接近均匀随机密钥空间不会因为口令短而明显坍缩。参数说明passphrase是用户口令的 UTF-8 字节不是界面上的 QStringm_keyLength是构造时选的枚举值Aes256截 32 字节Aes128截 16 字节。这个实现是 Demo 级简化版标准做法是 PBKDF2 加盐迭代避免口令被彩虹表暴力破解第 6 章会给替换代码。IV 的生成放在 encrypt 里用 OpenSSL 的RAND_bytesQByteArray iv(16, \0); RAND_bytes(reinterpret_castunsigned char*(iv.data()), iv.size());逻辑说明RAND_bytes是 OpenSSL 提供的安全随机数接口底层会利用操作系统熵源比qrand适合做加密凭据。16 字节恰好是 AES 的块大小也是 CBC 模式下 IV 的标准长度。这里先用 16 个\0初始化 QByteArray再让RAND_bytes覆盖它。3.2 加密流程输入字节 → PKCS7 填充 → EVP 加密 → Base64encrypt函数是完整加密链路的入口QByteArray AesEncryptor::encrypt(const QByteArray plainData, const QByteArray passphrase) const { QByteArray key deriveKey(passphrase); QByteArray iv(16, \0); RAND_bytes(reinterpret_castunsigned char*(iv.data()), 16); EVP_CIPHER_CTX* ctx EVP_CIPHER_CTX_new(); const EVP_CIPHER* cipher EVP_aes_256_cbc(); // 由 m_keyLength 决定 EVP_EncryptInit_ex(ctx, cipher, nullptr, reinterpret_castconst unsigned char*(key.constData()), reinterpret_castconst unsigned char*(iv.constData())); QByteArray cipherBuf(plainData.size() 16, \0); int outLen1 0, outLen2 0; EVP_EncryptUpdate(ctx, reinterpret_castunsigned char*(cipherBuf.data()), outLen1, reinterpret_castconst unsigned char*(plainData.constData()), plainData.size()); EVP_EncryptFinal_ex(ctx, reinterpret_castunsigned char*(cipherBuf.data()) outLen1, outLen2); QByteArray all iv cipherBuf.left(outLen1 outLen2); EVP_CIPHER_CTX_free(ctx); return all.toBase64(); }逻辑说明整个加密分三步。第一步 Init 把 cipher、key、iv 绑定到上下文第二步 Update 喂入全部明文第三步 Final 把缓冲区里剩余数据刷出来同时补上 PKCS7 填充。OpenSSL 的 EVP 接口会自动处理填充不需要手写 padding 逻辑但你要知道密文长度会因此变得和输入长度不成整数倍——输入 15 字节输出变成 16 字节输入 16 字节输出变成 32 字节。参数说明cipherBuf申请了plainData.size() 16是因为 CBC 加密最多多出一个块留 16 字节余量足够。outLen1是 Update 实际写入的字节数outLen2是 Final 实际写入的字节数两者相加才是有效密文长度不能直接用cipherBuf.size()否则会把未初始化的尾块一起 Base64 出去。all iv cipherBuf.left(...)把 IV 拼在密文头部解密时才能取出来。最后toBase64()输出不含换行的标准 Base64可以直接塞进 JSON 字段。3.3 解密流程Base64 解码 → 取 IV → EVP 解密 → 去填充decrypt是加密的逆过程但多了一步 IV 还原和填充校验QByteArray AesEncryptor::decrypt(const QByteArray cipherData, const QByteArray passphrase, bool* ok) const { if (ok) *ok false; QByteArray raw QByteArray::fromBase64(cipherData); if (raw.size() 17) return {}; // 16 字节 IV 加至少 1 字节密文 QByteArray key deriveKey(passphrase); QByteArray iv raw.left(16); QByteArray body raw.mid(16); EVP_CIPHER_CTX* ctx EVP_CIPHER_CTX_new(); const EVP_CIPHER* cipher EVP_aes_256_cbc(); EVP_DecryptInit_ex(ctx, cipher, nullptr, reinterpret_castconst unsigned char*(key.constData()), reinterpret_castconst unsigned char*(iv.constData())); QByteArray plainBuf(body.size() 16, \0); int outLen1 0, outLen2 0; EVP_DecryptUpdate(ctx, reinterpret_castunsigned char*(plainBuf.data()), outLen1, reinterpret_castconst unsigned char*(body.constData()), body.size()); int ret EVP_DecryptFinal_ex(ctx, reinterpret_castunsigned char*(plainBuf.data()) outLen1, outLen2); QByteArray plain plainBuf.left(outLen1 outLen2); EVP_CIPHER_CTX_free(ctx); if (ok) *ok (ret 1); return plain; }逻辑说明fromBase64得到的是当初all iv cipherBuf的原始字节raw.left(16)是 IVraw.mid(16)是真正的密文。解密时同样要先deriveKey因为密钥来源于口令而不是某个成员变量如果加密用了一次密钥、解密换成另一个口令这里就会得到不同的 key最终 Final 阶段填充校验失败ret返回 0ok置为 false。参数说明raw.size() 17的检查是为了防止空输入或损坏的 Base64 直接越界。plainBuf同样申请body.size() 16解密时 PKCS7 填充会被剥离实际有效数据可能比 body 短所以必须用outLen1 outLen2截断。EVP_DecryptFinal_ex是判断口令是否正确或密文是否被篡改的关键CBC 模式下它主要校验最后一组的填充格式返回 1 表示填充合法返回 0 表示末尾填充不符合预期此时解密结果不可信不建议直接使用。4. 界面落地实操加密文本、文件加密与 Base64 传输的完整走通4.1 主界面布局输入区、密钥区、结果区界面按四行组织第一行密钥长度下拉框第二行口令输入框第三行明文 QTextEdit第四行密文 QTextEdit再加并排两个按钮。核心代码MainWindow::MainWindow(QWidget* parent) : QWidget(parent) { auto* layout new QFormLayout(this); m_keyCombo new QComboBox(this); m_keyCombo-addItems({AES-128, AES-192, AES-256}); m_passEdit new QLineEdit(this); m_passEdit-setEchoMode(QLineEdit::Password); m_plainEdit new QTextEdit(this); m_cipherEdit new QTextEdit(this); auto* encryptBtn new QPushButton(tr(加密), this); auto* decryptBtn new QPushButton(tr(解密), this); layout-addRow(tr(密钥长度), m_keyCombo); layout-addRow(tr(口令), m_passEdit); layout-addRow(tr(明文), m_plainEdit); layout-addRow(tr(密文), m_cipherEdit); layout-addRow(encryptBtn, decryptBtn); connect(encryptBtn, QPushButton::clicked, this, MainWindow::onEncryptClicked); connect(decryptBtn, QPushButton::clicked, this, MainWindow::onDecryptClicked); }逻辑说明QFormLayout是做标签加输入框组合最省事的布局两列自动对齐不需要手算坐标。下拉框索引直接对应AesEncryptor::KeyLength的枚举值0 对应 Aes1281 对应 Aes1922 对应 Aes256读取时用static_cast转换即可。口令框用 Password 模式避免加密过程被别人瞥见密钥明文。参数说明成员变量m_keyCombo、m_passEdit、m_plainEdit、m_cipherEdit都是指针挂在this下由 Qt 对象树管理。connect 用新式语法编译期就能检查信号槽是否存在比字符串宏安全。4.2 文本加解密槽函数从按钮点击到结果回显加密按钮的槽函数是整个 Demo 用得最频繁的代码路径void MainWindow::onEncryptClicked() { QString pass m_passEdit-text(); QByteArray plain m_plainEdit-toPlainText().toUtf8(); AesEncryptor::KeyLength len static_castAesEncryptor::KeyLength( (m_keyCombo-currentIndex() 1) * 16); AesEncryptor enc(len); QByteArray cipher enc.encrypt(plain, pass.toUtf8()); m_cipherEdit-setPlainText(QString::fromLatin1(cipher)); }(currentIndex() 1) * 16把下拉框索引映射成 16/24/32正好是枚举值。AesEncryptor是栈上对象encrypt 调用完即销毁没有状态残留多点击几次也不会互相污染。编码说明明文统一用toUtf8()从 QString 转成字节流而不是toLatin1或toLocal8Bit。原因是 UTF-8 能覆盖所有 Unicode 字符中文、表情、生僻字到任何平台解密结果一致toLocal8Bit在中文 Windows 上是 GBK换到 Linux 上是 UTF-8同一段密文换个系统就解不回来了。Base64 字符串本身是 ASCII回填界面用QString::fromLatin1语意更准确。解密按钮的槽函数void MainWindow::onDecryptClicked() { QString pass m_passEdit-text(); QByteArray cipher m_cipherEdit-toPlainText().toLatin1(); AesEncryptor::KeyLength len static_castAesEncryptor::KeyLength( (m_keyCombo-currentIndex() 1) * 16); AesEncryptor enc(len); bool ok false; QByteArray plain enc.decrypt(cipher, pass.toUtf8(), ok); if (ok) m_plainEdit-setPlainText(QString::fromUtf8(plain)); else m_plainEdit-setPlainText(tr(解密失败口令错误或密文被篡改)); }逻辑说明解密后先检查ok再决定回填明文还是错误提示。这一步很重要EVP_DecryptFinal_ex返回 0 时 plain 里可能是一段残缺乱码直接回填会误导用户。解密侧按 UTF-8 还原和加密侧的toUtf8严格对称。4.3 文件加解密与二进制安全文本加解密走通后文件加密就是换数据源。核心区别是文件必须用QByteArray读取不能经过 QStringbool encryptFile(const QString inPath, const QString outPath, const QByteArray passphrase) { QFile in(inPath); if (!in.open(QIODevice::ReadOnly)) return false; QByteArray data in.readAll(); AesEncryptor enc(AesEncryptor::KeyLength::Aes256); QByteArray cipher enc.encrypt(data, passphrase); QFile out(outPath); if (!out.open(QIODevice::WriteOnly)) return false; out.write(cipher); return true; }逻辑说明QFile::readAll返回 QByteArray天然适合承载二进制。有人习惯用QString::fromLocal8Bit读文本文件再加密遇到含 0x00 的二进制文件字符串会在 NUL 处截断文件内容直接损坏。这个函数对文本、图片、压缩包一视同仁只要字节能读出来就能加密。参数说明cipher 是 Base64 字符串密文文件体积会比原文件大约 33%加上 IV 的 16 字节属于正常现象。readAll一次读入全部字节适合配置文件、证书这类 MB 级小文件GB 级大文件需要改成分块读取、分块调用EVP_EncryptUpdate的循环这个 Demo 不做。部署时可以把 inPath、outPath、passphrase 接到 QFileDialog 或命令行参数上上面的函数签名不需要变化。5. 踩坑排查Qt AES 加解密最常见的 5 个问题下面五条都是实际跑通这个 Demo 时踩过的坑没有一条是理论推导按“现象 → 原因 → 解决”写清楚。5.1 解密开头 16 字节乱码后面内容正常现象口令和密钥长度都对文本也能解出来但开头 16 字节是乱码从第 17 字节开始恢复正常。原因CBC 模式下第一个明文块直接和 IV 异或IV 错误只会影响前 16 字节后面的块依赖前一个密文块和 IV 无关。常见实现错误是加密时把随机 IV 转成了字符串存到别的地方解密时没取出来用了固定 IV 或全零 IV。解决统一“IV 拼在密文头部”的约定。encrypt 里all iv cipherBufdecrypt 里raw.left(16)取 IV、raw.mid(16)取密文。我通常会在类里加一个kIvLen 16常量把所有硬编码 16 的地方换成常量避免某个分支少写 16。5.2 中文加密解密后变成问号现象英文一切正常中文解密后变成一行问号或者一串“锟斤拷”式的乱码。原因这是编码问题不是 AES 问题。常见翻车点是界面层用toLatin1()转 QByteArray或者在解密后用fromLocal8Bit()还原。AES 只处理字节同样的字节被解释成 UTF-8 是中文被解释成 Latin-1 就是乱码。解决全工程统一 UTF-8。加密入口QString::toUtf8()解密出口QString::fromUtf8()文件读写不要用 QTextStream 默认编码直接 QByteArray 流。把这套规则写进代码规范后中文乱码再没出现过。5.3 EVP_EncryptInit_ex 返回 0错误栈报 bad key length现象new 了上下文Init 调用却失败用ERR_get_error看错误码提示 bad key length。原因密钥长度和 cipher 不匹配。下拉框选了 AES-128但deriveKey返回 32 字节或者反过来AES-256 的 cipher 给了 16 字节密钥。EVP 对长度非常严格多一字节少一字节都会拒绝。解决让枚举和 cipher 一一对应。建议写一个私有函数集中管理映射内部switch (m_keyLength)返回EVP_aes_128_cbc/EVP_aes_192_cbc/EVP_aes_256_cbc其他地方不再单独判断杜绝两边各写一份长度逻辑导致的不一致。5.4 密文放进 JSON 后解密失败Base64 里混入空白字符现象程序内直接解密成功把密文复制到 JSON 配置文件再读出来解密失败长度比预期长。原因Base64 文本在复制过程中混入了换行或空格或者嵌套进 JSON 后转义规则变了。Qt 的fromBase64能容忍部分空白但经过 JSON 序列化后行为不可控最好在解码前显式清理。解决fromBase64之前先清理字符串比如raw.remove( ).remove(\n).remove(\r)或者读取后先simplified()再去掉空格。另外尽量用文件读写密文做测试不要手抄一些在线工具生成的 Base64 带换行也是同一个坑。5.5 Windows MinGW 编译通过运行时报找不到 libcrypto-3.dll现象CMake 配置、编译都成功双击 exe 弹窗提示找不到 libcrypto-3.dll或“应用程序无法启动”。原因find_package(OpenSSL)找到的库路径不在系统 PATH或者下载的是 MSVC 版 OpenSSL。MinGW 链接器虽然能链上运行时导入表却不匹配甚至架构位数不一致也会这样。解决下载 MinGW 对应的 OpenSSL 预编译包CMake 配置时加-DOPENSSL_ROOT_DIR指过去部署时把 bin 目录下的 libcrypto-3.dll 和 libssl-3.dll 复制到 exe 同目录。想省掉 DLL 分发改用 OpenSSL 静态库并链接-static代价是编译产物变大。我实际项目里选了静态链接部署机上不用装任何运行库。6. 二次开发从 AES-CBC 升级到 GCM 与密钥托管方案拿到这个 Demo 后最常见的改造有三个方向CBC 换 GCM、口令派生换 PBKDF2、密钥做系统级托管。CBC 换 GCM 只动两个函数cipher 换成EVP_aes_256_gcm()加解密结束前多一步 tag 处理// 加密明文 Update 之后取出认证 tag EVP_EncryptUpdate(ctx, cipherBuf, outLen1, plain.constData(), plain.size()); EVP_EncryptFinal_ex(ctx, cipherBuf outLen1, outLen2); unsigned char tag[16]; EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_GET_TAG, 16, tag);// 解密密文 Update 之后把 tag 塞回去再 Final EVP_CIPHER_CTX_ctrl(ctx, EVP_CTRL_GCM_SET_TAG, 16, tag); int ret EVP_DecryptFinal_ex(ctx, cipherBuf outLen1, outLen2);GCM 多了合法性认证密文任何一个字节被改动Final 都会返回 0而不是像 CBC 那样解出一堆乱码。改动时把 tag 也拼进密文头部解密先取 IV 再取 tag最后取密文。口令派生升级成 PBKDF2 同样简单QByteArray salt(16, \0); RAND_bytes(reinterpret_castunsigned char*(salt.data()), 16); QByteArray key(32, \0); PKCS5_PBKDF2_HMAC(passphrase.constData(), passphrase.size(), reinterpret_castconst unsigned char*(salt.constData()), salt.size(), 10000, EVP_sha256(), key.size(), reinterpret_castunsigned char*(key.data()));10000 次迭代在桌面应用上只有毫秒级开销但能显著提高口令破解成本。salt 要随机生成并存在密文头部解密时读出来重新派生。验证改造是否成功我每次都会跑一组自动检查固定明文往返一致、篡改一个字节后 GCM 解密失败、换到 Linux 用同样的密文能解出来。把这组检查写成单元测试比手动点击按钮可靠得多。回想这个项目最开始的样子我用固定 IV 加密了所有调用直到某天发现两份相同明文的密文前缀一模一样才意识到 CBC 和 IV 随机性意味着什么。从那以后我每次改加密模块都会强制走一遍“往返 篡改 跨平台”三件套确认无误才提交。这个 Demo 把 IV、Base64、PKCS7 这些容易出错的边界都摊开写清楚了照着跑一遍比看十遍文档记得牢。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑