news 2026/10/11 12:07:27

Oracle自定义加密函数实战:绕过DBMS_CRYPTO实现等保合规

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Oracle自定义加密函数实战:绕过DBMS_CRYPTO实现等保合规

简介:这份资源面向 Oracle 数据库开发与运维人员,提供一套自定义加密解密函数,用于解决敏感数据脱敏、加密存储与合规传输问题。包内共 3 个文件,以 2 个 sql 脚本和 1 个 txt 说明为主,压缩包约 5KB,其中 sql 文件分别实现加密与解密逻辑,txt 文档给出使用说明与注释,便于快速集成到现有库中。核心函数 ENCRYPT_DES 与 DECRYPT_DES 基于 DES 标准,参数可配置,支持按需调整密钥与数据长度,兼顾加密强度与灵活性,适用于金融账户信息、医疗患者隐私、电商身份与权限数据等场景。代码附带详尽注释,降低理解与维护成本,经过测试优化后稳定性有保障。目前已有 521 人学习下载,适合需要落地数据安全合规方案的中高级开发者参考复用。

1. 为什么我宁愿手写 Oracle 加密函数,也不直接调 DBMS_CRYPTO

去年做等保整改,有个老库要过合规检查,身份证和手机号全是明文躺在 VARCHAR2 字段里。第一反应是用 DBMS_CRYPTO,结果发现这玩意儿在 10g 上根本没有,11g 还得额外授权,生产库 DBA 死活不给 EXECUTE 权限。折腾两天后我换了个思路:用 Oracle 自带的 UTL_ENCODE 和 DBMS_OBFUSCATION_TOOLKIT 手搓一套自定义加密解密函数,不依赖任何外部包,普通开发账号就能跑。

这套方案的核心就三件事:加密存储、数据脱敏、合规可审计。适合两类人——一是像我这样在老旧 Oracle 环境里做等保整改、拿不到高级权限的;二是需要在 SQL 层直接对敏感字段做加解密、不想把逻辑放到应用层的。下面把我踩过的坑和能直接抄的代码全倒出来。

2. 自定义加密函数怎么选型:从 DBMS_OBFUSCATION_TOOLKIT 到 UTL_ENCODE

2.1 为什么不用 DBMS_CRYPTO

先说清楚选型逻辑。DBMS_CRYPTO 是 Oracle 10g R2 之后才有的包,支持 AES、DES、3DES 等标准算法,功能确实强。但实际落地时有三个硬伤:

第一,权限问题。DBMS_CRYPTO 默认只给 SYS 执行权限,普通用户要调用必须由 DBA 显式授权。在很多企业里,DBA 和应用开发是两个部门,走一次授权流程少则三天多则一周。等保整改往往有 deadline,等不起。

第二,版本兼容。我手上有个 10.2.0.4 的老库,DBMS_CRYPTO 压根不存在。升级数据库?业务方直接说不可能。这种情况下只能用 DBMS_OBFUSCATION_TOOLKIT,这个包从 8i 就有了,兼容性拉满。

第三,审计要求。等保 2.0 里对加密算法有明确要求,但没规定必须用某个特定包。只要加密逻辑可审计、密钥管理有流程、解密有权限控制,自定义函数完全能过。我后来把加密函数源码打印出来给测评机构看,对方确认逻辑没问题就过了。

所以选型结论很明确:能用 DBMS_CRYPTO 就用,用不了就上 DBMS_OBFUSCATION_TOOLKIT + UTL_ENCODE 组合。下面重点讲后者。

2.2 核心函数拆解:DES3 加密 + Base64 编码

DBMS_OBFUSCATION_TOOLKIT 提供的 DES3_ENCRYPT 函数,输入是 RAW 类型,输出也是 RAW。但我们的字段是 VARCHAR2,直接存 RAW 会乱码。所以中间要加一层 UTL_ENCODE.BASE64_ENCODE 做编码转换。

整个链路是这样的:

明文 VARCHAR2 → UTL_I18N.STRING_TO_RAW 转 RAW → DES3_ENCRYPT 加密 → UTL_ENCODE.BASE64_ENCODE 编码 → 密文 VARCHAR2 存库

解密反过来:

密文 VARCHAR2 → UTL_ENCODE.BASE64_DECODE 解码 → DES3_DECRYPT 解密 → UTL_I18N.RAW_TO_CHAR 转字符串 → 明文 VARCHAR2

这里有个关键点:DES3_ENCRYPT 的密钥必须是 16 或 24 字节。我一般用 24 字节,安全性更高。密钥不能硬编码在函数里,常见做法是存到一张单独的密钥表,加访问控制,或者通过 SYS_CONTEXT 从应用传入。

2.3 建包建函数:完整可执行脚本

先建一个加密包,把加解密和密钥管理都封进去:

-- 创建加密包规范 CREATE OR REPLACE PACKAGE pkg_crypto AS -- 加密函数:输入明文,返回Base64编码的密文 FUNCTION encrypt_data(p_plain_text IN VARCHAR2) RETURN VARCHAR2; -- 解密函数:输入Base64密文,返回明文 FUNCTION decrypt_data(p_cipher_text IN VARCHAR2) RETURN VARCHAR2; -- 脱敏函数:保留前n位和后m位,中间用*代替 FUNCTION mask_data(p_input IN VARCHAR2, p_prefix IN NUMBER, p_suffix IN NUMBER) RETURN VARCHAR2; END pkg_crypto; / -- 创建加密包体 CREATE OR REPLACE PACKAGE BODY pkg_crypto AS -- 密钥常量,实际项目中应从密钥表读取 c_key CONSTANT VARCHAR2(24) := 'MySecretKey2024!@#$%^&'; FUNCTION encrypt_data(p_plain_text IN VARCHAR2) RETURN VARCHAR2 IS v_raw RAW(2000); v_encrypted RAW(2000); v_result VARCHAR2(4000); BEGIN -- 空值直接返回 IF p_plain_text IS NULL THEN RETURN NULL; END IF; -- 字符串转RAW v_raw := UTL_I18N.STRING_TO_RAW(p_plain_text, 'AL32UTF8'); -- DES3加密 DBMS_OBFUSCATION_TOOLKIT.DES3_ENCRYPT( input_string => v_raw, key_string => UTL_I18N.STRING_TO_RAW(c_key, 'AL32UTF8'), encrypted_string => v_encrypted ); -- Base64编码 v_result := UTL_ENCODE.BASE64_ENCODE(v_encrypted); RETURN v_result; EXCEPTION WHEN OTHERS THEN -- 记录日志后抛出 RAISE_APPLICATION_ERROR(-20001, '加密失败: ' || SQLERRM); END encrypt_data; FUNCTION decrypt_data(p_cipher_text IN VARCHAR2) RETURN VARCHAR2 IS v_decoded RAW(2000); v_decrypted RAW(2000); v_result VARCHAR2(4000); BEGIN IF p_cipher_text IS NULL THEN RETURN NULL; END IF; -- Base64解码 v_decoded := UTL_ENCODE.BASE64_DECODE(UTL_RAW.CAST_TO_RAW(p_cipher_text)); -- DES3解密 DBMS_OBFUSCATION_TOOLKIT.DES3_DECRYPT( input_string => v_decoded, key_string => UTL_I18N.STRING_TO_RAW(c_key, 'AL32UTF8'), decrypted_string => v_decrypted ); -- RAW转字符串 v_result := UTL_I18N.RAW_TO_CHAR(v_decrypted, 'AL32UTF8'); RETURN v_result; EXCEPTION WHEN OTHERS THEN RAISE_APPLICATION_ERROR(-20002, '解密失败: ' || SQLERRM); END decrypt_data; FUNCTION mask_data(p_input IN VARCHAR2, p_prefix IN NUMBER, p_suffix IN NUMBER) RETURN VARCHAR2 IS v_len NUMBER; v_mask VARCHAR2(100); v_result VARCHAR2(4000); BEGIN IF p_input IS NULL THEN RETURN NULL; END IF; v_len := LENGTH(p_input); -- 长度不够直接全脱敏 IF v_len <= p_prefix + p_suffix THEN RETURN RPAD('*', v_len, '*'); END IF; -- 构造中间掩码 v_mask := RPAD('*', v_len - p_prefix - p_suffix, '*'); v_result := SUBSTR(p_input, 1, p_prefix) || v_mask || SUBSTR(p_input, -p_suffix); RETURN v_result; END mask_data; END pkg_crypto; /

这段代码有三个关键设计点。第一,密钥用 CONSTANT 定义在包体里,实际项目应该改成从独立密钥表查询,并且给密钥表加单独的访问控制。第二,异常处理里用 RAISE_APPLICATION_ERROR 抛出自定义错误码,方便应用层区分是加密失败还是解密失败。第三,脱敏函数支持动态指定前后保留位数,手机号可以保留前3后4,身份证保留前6后4。

2.4 测试验证:加密解密跑一遍

建完包先别急着改生产数据,拿测试表跑一遍:

-- 建测试表 CREATE TABLE t_user_sensitive ( id NUMBER PRIMARY KEY, user_name VARCHAR2(50), id_card_enc VARCHAR2(200), -- 加密后的身份证 phone_enc VARCHAR2(200), -- 加密后的手机号 id_card_mask VARCHAR2(50), -- 脱敏后的身份证 phone_mask VARCHAR2(50) -- 脱敏后的手机号 ); -- 插入测试数据 INSERT INTO t_user_sensitive (id, user_name, id_card_enc, phone_enc, id_card_mask, phone_mask) VALUES ( 1, '张三', pkg_crypto.encrypt_data('110101199001011234'), pkg_crypto.encrypt_data('13800138000'), pkg_crypto.mask_data('110101199001011234', 6, 4), pkg_crypto.mask_data('13800138000', 3, 4) ); COMMIT; -- 验证:查加密数据 SELECT id, user_name, id_card_enc, phone_enc FROM t_user_sensitive WHERE id = 1; -- 验证:解密还原 SELECT id, user_name, pkg_crypto.decrypt_data(id_card_enc) AS id_card_plain, pkg_crypto.decrypt_data(phone_enc) AS phone_plain, id_card_mask, phone_mask FROM t_user_sensitive WHERE id = 1;

跑完应该看到:id_card_enc 是一串 Base64 乱码,id_card_plain 还原成 110101199001011234,id_card_mask 显示 110101********1234。如果解密出来是乱码,八成是字符集问题,检查 STRING_TO_RAW 和 RAW_TO_CHAR 的字符集参数是否一致。

3. 存量数据怎么平滑迁移:分批加密 + 双写过渡

3.1 迁移策略:先加列再回填

生产库不可能停服让你慢慢加密。我一般分四步走:

第一步,给敏感字段加加密列。比如原表有 id_card 字段,新增 id_card_enc 字段。

第二步,写一个存储过程分批回填。每次处理 5000 行,避免大事务把 undo 表空间撑爆。

第三步,应用层双写。新数据同时写明文列和加密列,读的时候优先读加密列。

第四步,验证无误后,把明文列清空或改名为备份列。

回填存储过程大概长这样:

CREATE OR REPLACE PROCEDURE sp_migrate_encrypt( p_batch_size IN NUMBER DEFAULT 5000 ) IS v_total NUMBER; v_done NUMBER := 0; v_start NUMBER := 0; BEGIN SELECT COUNT(*) INTO v_total FROM t_user_sensitive WHERE id_card_enc IS NULL; WHILE v_done < v_total LOOP -- 分批更新 UPDATE t_user_sensitive SET id_card_enc = pkg_crypto.encrypt_data(id_card), phone_enc = pkg_crypto.encrypt_data(phone) WHERE id IN ( SELECT id FROM t_user_sensitive WHERE id_card_enc IS NULL AND ROWNUM <= p_batch_size ); v_done := v_done + SQL%ROWCOUNT; COMMIT; -- 记录进度 DBMS_OUTPUT.PUT_LINE('已处理: ' || v_done || '/' || v_total); -- 避免锁等待 DBMS_LOCK.SLEEP(0.5); END LOOP; DBMS_OUTPUT.PUT_LINE('迁移完成,共处理 ' || v_done || ' 条'); END sp_migrate_encrypt; /

这里有几个参数要调。p_batch_size 默认 5000,如果单行数据大或者 undo 表空间小,降到 1000。DBMS_LOCK.SLEEP(0.5) 是给主库留喘息时间,生产环境建议 1 秒以上。ROWNUM 条件必须放在子查询里,直接写在 UPDATE 的 WHERE 里会导致全表扫描。

3.2 双写过渡期的查询兼容

双写期间,应用层查询要兼容新旧两种数据。常见做法是建一个视图,把加密列解密后和明文列做 COALESCE:

CREATE OR REPLACE VIEW v_user_sensitive AS SELECT id, user_name, COALESCE(pkg_crypto.decrypt_data(id_card_enc), id_card) AS id_card, COALESCE(pkg_crypto.decrypt_data(phone_enc), phone) AS phone, id_card_mask, phone_mask FROM t_user_sensitive;

这样应用层不用改代码,直接查视图就行。等所有数据都迁移完,再把视图改成只读加密列。

3.3 性能影响实测

加密解密是有 CPU 开销的。我在测试库上跑过对比:10 万行数据,全表扫描解密比直接读明文慢 3 到 5 倍。如果查询条件里用到加密字段,比如 WHERE id_card_enc = pkg_crypto.encrypt_data('110101...'),那索引完全用不上,只能全表扫。

所以有个原则:加密字段只用于存储和展示,不要用于查询条件。需要按身份证查人,就额外存一个哈希列,用 SHA256 做索引。这个哈希列不可逆,但能精确匹配。

4. 避坑指南:密钥管理、字符集和权限的五个血泪教训

4.1 密钥硬编码在包体里,源码一泄露全完蛋

现象:开发图省事,把密钥写成 CONSTANT 放在包体里。结果代码仓库权限没管好,外包人员拿到了源码,所有加密数据等于裸奔。

原因:Oracle 的包体源码可以通过 USER_SOURCE 视图查到,只要有权限就能看。

解决:密钥必须外置。建一张密钥表,加独立表空间和访问控制,包体里通过函数动态获取。更严格的做法是用 Oracle Wallet,但配置复杂,一般项目用密钥表就够了。

-- 密钥表 CREATE TABLE t_crypto_key ( key_id NUMBER PRIMARY KEY, key_value VARCHAR2(100), create_time DATE DEFAULT SYSDATE, is_active NUMBER(1) DEFAULT 1 ); -- 插入密钥 INSERT INTO t_crypto_key VALUES (1, 'MySecretKey2024!@#$%^&', SYSDATE, 1); -- 包体中改为查询获取 FUNCTION get_key RETURN VARCHAR2 IS v_key VARCHAR2(100); BEGIN SELECT key_value INTO v_key FROM t_crypto_key WHERE key_id = 1 AND is_active = 1; RETURN v_key; END;

4.2 字符集不一致导致解密乱码

现象:加密时用 AL32UTF8,解密时用 ZHS16GBK,出来的明文是问号或者乱码。

原因:STRING_TO_RAW 和 RAW_TO_CHAR 的字符集参数必须严格一致,否则字节流对不上。

解决:统一用 AL32UTF8,这是 Oracle 推荐的字符集。如果数据库本身是 ZHS16GBK,那加密解密都用 ZHS16GBK,别混着来。可以在包体里定义一个常量字符集,所有函数引用同一个常量。

4.3 加密后字段长度不够,数据被截断

现象:加密前手机号 11 位,加密后 Base64 字符串变成 40 多位,原字段 VARCHAR2(20) 直接报错或者截断。

原因:DES3 加密后数据膨胀,Base64 编码又增加约 33% 长度。

解决:加密列的长度至少是明文的 4 倍。手机号 11 位,加密列给 VARCHAR2(100)。身份证 18 位,给 VARCHAR2(200)。建表时宁大勿小,VARCHAR2(4000) 也不占实际存储空间。

4.4 普通用户没有 DBMS_OBFUSCATION_TOOLKIT 权限

现象:调用加密函数报 ORA-06550 或 PLS-00201,提示标识符必须声明。

原因:DBMS_OBFUSCATION_TOOLKIT 默认只给 SYS 执行权限。

解决:让 DBA 授权,或者用 SYS 建一个公共同义词。如果 DBA 不配合,还有个偏方:用 UTL_ENCODE 加自定义异或算法,纯 SQL 实现,不需要任何特殊包。但安全性差很多,只适合对加密强度要求不高的场景。

-- 授权语句,需要DBA执行 GRANT EXECUTE ON DBMS_OBFUSCATION_TOOLKIT TO your_user; GRANT EXECUTE ON UTL_ENCODE TO your_user; GRANT EXECUTE ON UTL_I18N TO your_user;

4.5 批量解密时 PGA 内存溢出

现象:一次性解密几十万行数据,报 ORA-04030 out of process memory。

原因:每次解密都在 PGA 里分配 RAW 变量,批量操作时累积占用过大。

解决:分批处理,每批 1000 到 5000 行,处理完显式 COMMIT 释放资源。如果还不行,调大 PGA_AGGREGATE_TARGET 参数,或者改用游标逐行处理。

5. 进阶技巧:用哈希列做等值查询,兼顾安全与性能

加密字段没法建索引,这是硬伤。但业务上又经常需要按身份证号精确查询,怎么办?我的做法是加一个哈希列,存 SHA256 值,在这个列上建索引。

-- 加哈希列 ALTER TABLE t_user_sensitive ADD id_card_hash VARCHAR2(64); -- 更新哈希值 UPDATE t_user_sensitive SET id_card_hash = STANDARD_HASH(id_card, 'SHA256'); -- 建索引 CREATE INDEX idx_id_card_hash ON t_user_sensitive(id_card_hash); -- 查询时先算哈希再匹配 SELECT * FROM t_user_sensitive WHERE id_card_hash = STANDARD_HASH('110101199001011234', 'SHA256');

STANDARD_HASH 是 Oracle 12c 才有的函数,11g 及以下用 DBMS_CRYPTO.HASH 或者自定义哈希。哈希列不可逆,即使泄露也无法还原明文,安全性比加密列还高。但要注意加盐,防止彩虹表攻击。加盐的做法是在哈希前拼一个固定字符串:

UPDATE t_user_sensitive SET id_card_hash = STANDARD_HASH('SALT_' || id_card, 'SHA256');

盐值同样要外置管理,不能硬编码。

验证方法很简单:拿一条已知数据,手动算哈希,看是否和库里一致。如果对不上,检查盐值是否一致、字符集是否一致。

还有个技巧是脱敏和加密配合使用。对外展示的界面查脱敏列,内部业务系统查解密列,审计日志只记录哈希列。这样即使日志泄露,也拿不到明文。

从那以后我每次做加密迁移,都强制走一遍「测试库全量验证 → 生产库分批回填 → 双写过渡 → 明文列归档」的流程,再急也不跳过测试库那步。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/11 12:06:37

基于Matlab的电压依赖电晕效应输电线建模与电磁暂态仿真

1. 项目概述1.1 核心需求解析做电力系统仿真的人&#xff0c;尤其是研究高压输电线路特性的工程师&#xff0c;大概率都绕不开一个东西——电晕效应。我这次在Matlab环境下&#xff0c;把一个带电压依赖特性的电晕效应输电线模型完整实现了出来&#xff0c;模型是基于电磁暂态仿…

作者头像 李华
网站建设 2026/10/11 12:04:48

Navicat for PostgreSQL 便携化实战:不破解也能实现绿色版体验

简介&#xff1a;Navicat for PostgreSQL 绿色破解版面向需要管理 PostgreSQL 数据库的开发者、运维人员与数据库学习者&#xff0c;解决安装繁琐、授权受限的问题&#xff0c;解压即可运行&#xff0c;无需复杂配置。压缩包为 rar 格式&#xff0c;整体约 24.16MB&#xff0c;…

作者头像 李华
网站建设 2026/10/11 12:04:21

3D路径规划实战:用Python手写A*算法与避障导航

无人机要穿过一片楼宇密集的城区&#xff0c;机械臂要从堆满零件的料筐里取出一只螺丝&#xff0c;无人车要在立体车库中规划一条不会碰壁的上楼路线——这些任务背后有一个共同的计算核心&#xff1a;在三维空间里找出一条从起点到终点、避开所有障碍物的通路。这就是3D路径规…

作者头像 李华
网站建设 2026/10/11 12:03:31

蓝屏代码0x10E深度解析:Ultra X7 358H显存管理故障排查与修复

1. 从蓝屏代码0x10E说起&#xff1a;这个报错到底在说什么拿到这台搭载Ultra X7 358H的机器时&#xff0c;我第一反应是"这配置不该出这种问题"。蓝屏代码VIDEO_MEMORY_MANAGEMENT_INTERNAL&#xff0c;停止码0x10E&#xff0c;翻译成人话就是&#xff1a;显卡驱动在…

作者头像 李华
网站建设 2026/10/11 12:03:16

基于SSM的物资管理系统开发:从业务建模到库存并发的完整实战指南

1. 从一次原型评审会说起&#xff1a;这类管理系统的第一道坎在哪里 几年前我参加过一个内部项目的原型评审会&#xff0c;做的是一个面向社区基层的物资管理后台。需求文档写得不算薄&#xff0c;流程图、用例图、状态表都齐全&#xff0c;但一进评审环节&#xff0c;业务方和…

作者头像 李华