ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Oracle自定义加密解密函数实战:基于DBMS_CRYPTO的AES256等保整改方案

2026/10/3 4:53:03 拓冰建站 浏览量
Oracle自定义加密解密函数实战:基于DBMS_CRYPTO的AES256等保整改方案 简介面向 Oracle 数据库开发与运维人员的自定义加密解密函数包基于 DES 加密标准实现敏感字段的加密、解密与脱敏处理适用于金融、医疗、电商等需要对账户信息、身份证、密码及交易记录做合规保护的场景。函数库提供 ENCRYPT_DES 与 DECRYPT_DES 两个函数参数可灵活配置密钥与数据长度本地部署或云数据库均可无缝集成。资源共 3 个文件含 2 个 SQL 脚本与 1 个说明文档txt压缩包整体约 5KB其中 SQL 文件包含完整函数代码及详尽注释txt 文档辅助说明使用要点结构轻量、便于直接部署。目前已有 521 人学习下载。读者可通过注释文档快速理解加密原理与调用方式根据业务需求调整加密密钥和脱敏策略简化加密解密流程并降低后续维护成本适合需要快速落地数据安全方案的 Oracle 技术人员参考使用。1. Oracle自定义加密解密函数等保整改和脱敏需求下的第一落点做 Oracle 数据安全改造最绕不开的一件事就是 Oracle 自定义加密解密函数。等保测评整改单上写着“敏感字段明文存储”业务方又要求身份证、手机号不能直接查出来底层数据库还不方便整体启用 TDE——这时候用 DBMS_CRYPTO 封装一组自定义函数是绝大多数 Oracle 项目走的路。这个方案不依赖外部加密机不用改应用连接串把加密逻辑收拢在数据库内部配合视图和触发器就能同时解决加密存储和脱敏查询两件事。适合谁既要应付合规审计又不想把业务系统翻个底朝天的 DBA 和应用负责人。本文按“选型 → 实现 → 落地 → 避坑 → 验证”的顺序讲一套可以直接抄的完整做法。2. 用 DBMS_CRYPTO 封装自定义加密解密函数算法选型与最小可运行包2.1 为什么选 DBMS_CRYPTO而不是 DBMS_OBFUSCATION_TOOLKITOracle 自带两套加密 API早期版本里的 DBMS_OBFUSCATION_TOOLKIT 和 10g 之后主推的 DBMS_CRYPTO。DBMS_OBFUSCATION_TOOLKIT 只支持 DES 和 3DES算法老旧块长度和密钥长度都跟不上现在等保对算法强度的要求。DBMS_CRYPTO 支持 AES128/192/256、RC4、3DES还能自己选 CBC/ECB 链模式和 PKCS5 填充方式灵活性高出一截。我一般在生产库上只用 AES256 CBC PKCS5 组合。AES256 满足“商用密码算法强度”的常见审计口径CBC 模式下同样的明文因为初始向量不同每次加密结果都不一样避免 ECB 那种“相同明文相同密文”的明显特征。PKCS5 填充解决 Oracle VARCHAR2 长度不固定带来的块对齐问题。还有一个选型细节DBMS_CRYPTO 默认只处理 RAW 类型不直接接受 VARCHAR2。业务表里存的都是字符所以自定义函数里必须先用 UTL_I18N.STRING_TO_RAW 做字符集转换解密后再用 UTL_I18N.RAW_TO_CHAR 转回来。这一步漏掉中文乱码和 ORA-06502 就会轮番来找你。另外一个需要提前确定的点是加密后的密文以什么形式落库。很多人图省事把 RAW 直接隐式转成 VARCHAR2 存进表里结果取出来再解密时发现 RAW 被截断或格式走样。我的做法是统一用 RAW 类型字段存密文。RAW 不会做字符集转换字节内容原样保留加解密结果最稳定。如果业务上实在只能接受 VARCHAR2那必须用 RAWTOHEX 函数把 RAW 转成十六进制字符串解密前再用 HEXTORAW 还原不要直接塞 RAW。2.2 最小可运行包AES256 加密函数与解密函数的完整代码下面这套包体和包规范是我在项目中反复用的底子包含加密函数、解密函数和一个测试用的校验函数。先创建包规范CREATE OR REPLACE PACKAGE pkg_crypto_util AS -- 加密入参为明文 VARCHAR2出参为加密后的 RAW FUNCTION fn_encrypt_str(p_plain IN VARCHAR2) RETURN RAW; -- 解密入参为密文 RAW出参为明文 VARCHAR2 FUNCTION fn_decrypt_raw(p_cipher IN RAW) RETURN VARCHAR2; END pkg_crypto_util; /包规范只声明接口不写实现。这里故意没把密钥放进去密钥属于包体内部细节外部调用者不需要知道。接下来是包体CREATE OR REPLACE PACKAGE BODY pkg_crypto_util AS -- 32 字节 AES256 密钥十六进制表示 gc_key RAW(32) : HEXTORAW(1A2B3C4D5E6F708192A3B4C5D6E7F809); -- 16 字节初始向量 gc_iv RAW(16) : HEXTORAW(0F1E2D3C4B5A69788796A5B4C3D2E1F0); FUNCTION fn_encrypt_str(p_plain IN VARCHAR2) RETURN RAW IS v_cipher RAW(4000); BEGIN -- 字符转 RAW然后 AES256 加密CBC 链模式PKCS5 填充 v_cipher : DBMS_CRYPTO.ENCRYPT( src UTL_I18N.STRING_TO_RAW(p_plain, AL32UTF8), typ DBMS_CRYPTO.ENCRYPT_AES256 DBMS_CRYPTO.CHAIN_CBC DBMS_CRYPTO.PAD_PKCS5, key gc_key, iv gc_iv ); RETURN v_cipher; END fn_encrypt_str; FUNCTION fn_decrypt_raw(p_cipher IN RAW) RETURN VARCHAR2 IS v_plain VARCHAR2(4000); BEGIN -- 先用密钥解密得到 RAW再按数据库字符集转回 VARCHAR2 v_plain : UTL_I18N.RAW_TO_CHAR( DBMS_CRYPTO.DECRYPT( src p_cipher, typ DBMS_CRYPTO.ENCRYPT_AES256 DBMS_CRYPTO.CHAIN_CBC DBMS_CRYPTO.PAD_PKCS5, key gc_key, iv gc_iv ), AL32UTF8 ); RETURN v_plain; END fn_decrypt_raw; END pkg_crypto_util; /逻辑说明加密函数先用 UTL_I18N.STRING_TO_RAW 把输入字符串按 AL32UTF8 转成 RAW 字节流再交给 DBMS_CRYPTO.ENCRYPT 处理。typ 参数是三个常量的和这是 Oracle 的写法表示“算法 链模式 填充方式”的组合。解密函数是反过程注意 DECRYPT 的 src 参数接的是 RAW 类型所以调用方如果存的是十六进制字符串必须先 HEXTORAW 再传进来。参数说明gc_key 是 32 字节密钥对应 AES256gc_iv 是 16 字节初始向量。这两个值在代码里是写死的示例生产环境绝对不能直接用。密钥至少 32 字节iv 必须 16 字节否则 DBMS_CRYPTO 直接报 ORA-28817“密钥长度无效”。测试调用-- 加解密往返测试 SELECT pkg_crypto_util.fn_decrypt_raw( pkg_crypto_util.fn_encrypt_str(身份证号110101199001011234) ) AS plain_text FROM dual;正常返回原串。这里有个关键的验证习惯加密同一段明文两次观察密文是否不同。CBC 模式因为有 iv 参与两次密文应该不一样。如果两次密文完全一致说明你的调用方式有问题ECB 特征太明显审计一眼就能看出来。2.3 密钥从哪来硬编码不行存哪更稳密钥放在包体里虽然能用但等保测评专家看到 PL/SQL 源码里躺着 32 字节的十六进制密钥多半会开一条整改项“加密密钥硬编码于数据库源码中”。我见过不止一个项目在这里被卡住。常见做法有三种。第一种是放在数据库外部应用启动时通过环境变量或配置文件读取再传给数据库函数。这种适合应用层和数据库层能协同改造的场景。第二种是存在单独的表里通过权限控制只有特定角色能读。这种比硬编码强但表数据可能被导出密钥和密文一旦一起泄露就没意义了。第三种是把主密钥放在外部密钥管理系统数据库只存密钥标识应用解密时通过接口取主密钥再传进来。我的建议是如果暂时没有外部 KMS至少把密钥拆成两段分存比如一段存表、一段藏在包体常量里函数内拼接后使用。这样数据库文件被拖走没有另一段也拼不出完整密钥。等保测评对“密钥与密文分开存储”有明确要求这种拆法在多数场景能过审。密钥轮换也要提前考虑我的做法是表里加一个 key_version 字段加密时带上当前版本号解密时根据版本号选对应密钥。这样轮换密钥时旧数据还能正常解不用一次性全表重加密。3. 把自定义加密函数接进业务加密存储与脱敏查询的实际落地3.1 加密存储用触发器让业务代码无感封装好函数之后最直接的应用就是写操作自动加密。我不建议在每个 INSERT 语句里手动调用加密函数业务代码散布太多地方后面漏改一个就出大问题。更稳的做法是在表上建 BEFORE INSERT 触发器统一把敏感字段加密。-- 用户表mobile 字段存密文 CREATE TABLE t_user ( user_id NUMBER PRIMARY KEY, user_name VARCHAR2(50), mobile RAW(200), id_card RAW(200), create_time DATE DEFAULT SYSDATE ); -- 插入前自动加密 CREATE OR REPLACE TRIGGER trg_user_encrypt_insert BEFORE INSERT ON t_user FOR EACH ROW BEGIN -- 只在传入明文时加密如果调用方已经传了密文可以按需跳过 :NEW.mobile : pkg_crypto_util.fn_encrypt_str(:NEW.mobile); :NEW.id_card : pkg_crypto_util.fn_encrypt_str(:NEW.id_card); END; /逻辑说明触发器在 INSERT 执行前把字段值拦截下来调用包里的加密函数处理后写回 :NEW 伪记录。业务代码完全不用改还按原样 INSERT 明文落库时自动变成密文。UPDATE 场景同样处理建一个 BEFORE UPDATE 触发器复制上面逻辑即可。参数说明RAW(200) 的长度不是拍脑袋定的。AES256 加密的密文长度最小 32 字节且按 16 字节块向上取整。一个 11 位手机号加密后大约 32 字节18 位身份证号加密后也是 32 字节200 字节的余量足够未来扩展。如果字段被定义成 VARCHAR2 且存的是 RAWTOHEX 字符串那长度要按“密文字节数 × 2”再加余量30 字节的密文就要分配 64 以上长度的 VARCHAR2。3.2 脱敏查询加密后不能直读加一层视图做控制加密存储之后最简单的 SELECT 查出来是一串乱码业务方不会接受。常见做法是再包一层视图在视图里解密并按用户权限决定是返回明文还是脱敏值。-- 脱敏函数手机号中间四位打星 CREATE OR REPLACE FUNCTION fn_mask_mobile(p_mobile VARCHAR2) RETURN VARCHAR2 IS BEGIN -- 入参是解密后的明文手机号 RETURN SUBSTR(p_mobile, 1, 3) || **** || SUBSTR(p_mobile, 8); END; / -- 查询视图普通用户看脱敏值授权角色看明文 CREATE OR REPLACE VIEW v_user_secure AS SELECT user_id, user_name, CASE WHEN SYS_CONTEXT(USERENV, CURRENT_USER) IN (APP_ADMIN) THEN pkg_crypto_util.fn_decrypt_raw(mobile) ELSE fn_mask_mobile(pkg_crypto_util.fn_decrypt_raw(mobile)) END AS mobile_mask, CASE WHEN SYS_CONTEXT(USERENV, CURRENT_USER) IN (APP_ADMIN) THEN pkg_crypto_util.fn_decrypt_raw(id_card) ELSE pkg_crypto_util.fn_decrypt_raw(id_card) -- 身份证默认只给管理员看普通用户返回空 END AS id_card FROM t_user;逻辑说明视图里用 SYS_CONTEXT 判断当前登录用户实现行级和列级权限控制。普通用户查出来是138****0012这种脱敏格式管理员用户拿明文。CASE 里解密函数被调多次性能上可以接受因为单行数据量不大。如果表特别大建议把解密函数的结果先算一遍再用避免每行重复解密两次。参数说明脱敏规则按业务来手机号保前 3 后 4身份证保前 6 后 4中间统一打星。这种“保留部分明文”的做法在实际审计中经常被问到要准备好依据说明“为什么保留这几位”。一般理由是手机号前 3 位是运营商号段后 4 位用于用户本人核对身份证前 6 位是行政区划后 4 位是校验号。审计能接受这个解释。3.3 模糊检索加密字段查不准用冗余列缓解加密后最头疼的问题就是模糊查询。AES 是块加密WHERE mobile LIKE %1234%这种 SQL 在密文上根本跑不了。完全禁止模糊查询在业务上行不通我用的折中方案是“冗余脱敏列 密文后几位”。-- 给表增加手机号后四位明文冗余列 ALTER TABLE t_user ADD mobile_tail4 VARCHAR2(4); -- 插入或更新时同时写入后四位 CREATE OR REPLACE TRIGGER trg_user_tail4 BEFORE INSERT OR UPDATE ON t_user FOR EACH ROW BEGIN -- 取明文后四位用于按尾号检索 :NEW.mobile_tail4 : SUBSTR(:NEW.mobile, -4); END; / -- 按后四位查用户 SELECT * FROM v_user_secure WHERE mobile_tail4 0012;逻辑说明冗余列存的是明文后四位不加密。这本身会泄露一部分信息所以只能存“可用于检索但不足以完整还原”的片段比如手机号后四位。完整手机号是 11 位知道后四位不影响整体安全性。如果业务需要按身份证完整号码精确查我一般加一个“加密身份证号后六位”的冗余字段查询时先把用户输入的身份证后六位加密成密文再和冗余列比对。参数说明冗余列的取舍要在需求阶段跟业务讲清楚“加密必然牺牲检索能力”。一点都不允许降级检索的字段就别放进这套加密体系否则就得引入全文索引或者外部搜索引擎成本完全不是一个量级。等保测评关注的是敏感字段不直接暴露冗余列只要控制好长度和展示权限审计一般能通过。3.4 等保命令与审计视角量化交易这类高频场景怎么控制解密开销热词里有一个“量化交易数据访问和存储安全加密方案设计”这类场景的特点是高频、大批量、对延迟敏感。加密解密本身有 CPU 开销如果每条查询都在视图里对全表做解密性能会很难看。我的习惯是给这类场景单独开“解密专用通道”而不是让所有查询都走带脱敏逻辑的视图。具体做法是-- 高频交易场景只取当前交易日需要解密的少量记录 SELECT user_id, pkg_crypto_util.fn_decrypt_raw(mobile) AS mobile FROM t_user WHERE create_time TRUNC(SYSDATE) AND create_time TRUNC(SYSDATE) 1 AND user_id IN (SELECT user_id FROM t_trade_today);逻辑说明TRUNC(SYSDATE) 截断到当天零点把扫描范围缩小到当日数据再用子查询过滤出真正要处理的用户。加密函数只对结果集里的少量行执行而不是全表逐行解密。配合索引这种方案在高频场景下能扛住几十万行级别的日处理量。参数说明如果子查询出来的结果集仍然很大建议分批处理每批 500 到 1000 行避免一次性解密把 CPU 打满。DBMS_CRYPTO 的解密操作是纯 CPU 计算并行度过高会导致库上其他业务明显变慢。这个坑我在生产上踩过批量解密任务一跑正常 OLTP 查询的响应时间直接翻倍。4. 避坑自定义加密函数上线后最常见的 5 个翻车现场4.1 现象解密出来字符串后半截乱码或者直接报 ORA-06502原因源数据字符集和目标字符集不一致。加密时用 AL32UTF8 转 RAW解密时如果数据库实际字符集是 ZHS16GBKRAW_TO_CHAR 的转换结果就可能是乱码。另一种情况是 VARCHAR2 字段存了截断的十六进制字符串少了字符导致 HEXTORAW 失败。解决加密和解密函数里都显式指定字符集不要依赖数据库默认参数。我习惯统一用 AL32UTF8不管是 ZHS16GBK 还是 AL32UTF8 的库字符串先转成 AL32UTF8 字节流再加密解密后按同一字符集转回。注意如果数据库字符集是 ZHS16GBK转成 AL32UTF8 后字节数会变长字段长度要留够否则插入时报 ORA-12899 值过大。数据类型尽量用 RAW 存密文配合 RAWTOHEX 的方案必须保证十六进制字符串完整。4.2 现象更新密钥后旧数据全部解密报错原因数据库里的存量数据是用旧密钥加密的包体里密钥一改新密钥解旧密文AES 算法直接报 ORA-28817 或返回乱码。解决上线前就设计好密钥版本机制。表里增加 key_version 字段加密函数把版本号拼进密文头部比如密文前 4 字节存版本号后面才是真正的密文。解密函数先读前 4 字节得到版本号再按版本号选对应的密钥。这样密钥轮换时旧数据按旧密钥解新数据按新密钥解两边互不影响。注意这个方案下密文长度会多出几个字节字段定义要留余量。另外密钥轮换要选业务低峰期先小范围验证再全量执行。4.3 现象加密列上的 WHERE 查询变成全表扫SQL 慢到超时原因密文是随机的B 树索引对加密列基本失效。range scan 和 equality 都可能走成 full table scan。很多人没意识到加密字段上的索引在加密后约等于废了。解决不要在加密列上建索引要用冗余脱敏列或密文后几位来支撑查询。精确匹配场景用“加密后的值”等值查询前提是查询条件也做同样加密。比如身份证号精确查程序先把用户输入的身份证号调用 fn_encrypt_str 转成密文再拿密文等值匹配。这个方法可行是因为同一个字符串在相同密钥和 iv 下加密结果不变。但要注意如果你每次加密用的 iv 是随机生成的那这种等值匹配就失效了必须用固定 iv 或者在密文前拼一个确定性校验值。我在生产里更推荐“明文冗余列 权限管控”的方案确定性加密用不好反而引来更多麻烦。4.4 现象存储过程包状态被丢弃报 ORA-04068 终止会话原因热词里那条“oracle 为什么会出现 包状态 被丢弃”指的就是这个。包体里如果有包级变量或者依赖的表结构变更过会导致包状态失效。加密解密包在会话里被反复调用一旦其他会话执行了 ALTER 操作这个会话里的包状态就作废再调用就报错。解决包体里尽量避免使用包级变量保存状态所有参数显式传入。我上面贴的代码里 gc_key 和 gc_iv 是常量不会产生会话级状态所以一般不会触发这个问题。如果确实需要缓存密钥用 DBMS_SESSION.SET_CONTEXT 存到会话上下文里而不是包体变量。业务代码遇到 ORA-04068 要能自动重连或重新调用一次不能直接把异常抛给用户。4.5 现象等保测评现场开了整改单“未对敏感字段加密”原因加密函数做了但没写在等保自查报告里或者审计日志没开启测评人员看不到任何加密动作记录。技术上做到位了材料上没跟上一样算不合格。解决提测时就把加密方案整理成文档包含算法名称、密钥长度、密钥存储位置、密钥轮换周期、脱敏规则说明。同时开启统一审计关注加解密函数相关的调用记录。Oracle 19c 里可以用 UNIFIED_AUDIT 配置等保命令把对关键表的 INSERT、UPDATE、SELECT 都记录入库。测评专家看的不只是能不能加密还要看是不是有完整的“数据安全生命周期管理”痕迹。5. 验证加密函数正确性的三个技巧从单元测试到等保材料准备5.1 用 DBMS_RANDOM 造大批量数据做回归测试加密函数写完之后最容易翻车的是边界场景。先建一张测试表灌进去各种长度的字符串再批量做加解密往返校验-- 构造 10 万行随机测试数据覆盖长度边界 CREATE TABLE t_crypto_test AS SELECT rownum AS id, DBMS_RANDOM.STRING(A, DBMS_RANDOM.VALUE(1, 50)) AS plain_text FROM dual CONNECT BY LEVEL 100000; -- 全部加密再解密比对是否和原文一致 SELECT COUNT(*) AS mismatch_cnt FROM ( SELECT plain_text, pkg_crypto_util.fn_decrypt_raw( pkg_crypto_util.fn_encrypt_str(plain_text) ) AS roundtrip_text FROM t_crypto_test ) WHERE plain_text roundtrip_text;逻辑说明DBMS_RANDOM.STRING 生成随机纯字母字符串长度从 1 到 50 随机分布。第二段 SQL 把每行做加解密往返然后对比明文是否一致。mismatch_cnt 为 0 说明函数在常规长度下没问题。这个测试表要放在目标库的测试环境里跑别在生产库直接执行。参数说明DBMS_RANDOM.STRING 的第二个参数是长度上限我这里取 50覆盖 VARCHAR2 常见场景。如果要测中文乱码把生成规则换成随机中文字符串用 CONNECT BY 配合 DBMS_RANDOM.VALUE 从汉字列表里取字。加密函数在事务里调用超大批量测试建议分段提交否则 UNDO 会撑得很大。5.2 密文一致性与长度边界验证加密函数还有个容易被忽略的问题同一明文多次加密结果不同。CBC 模式下 iv 固定时结果相同iv 随机时不同。这直接影响数据迁移和比对校验。-- 验证密文长度是否符合预期 SELECT id, LENGTH(RAWTOHEX(pkg_crypto_util.fn_encrypt_str(plain_text))) AS hex_len FROM t_crypto_test WHERE rownum 5; -- 验证同一明文加密两次是否一致 SELECT pkg_crypto_util.fn_encrypt_str(测试数据) AS enc_once, pkg_crypto_util.fn_encrypt_str(测试数据) AS enc_twice FROM dual;逻辑说明第一段 SQL 看密文十六进制长度是否按块对齐。AES256 块大小是 16 字节所以明文长度小于等于 16 字节时密文长度是 32 字节十六进制也就是 64 个十六进制字符明文长度 17 到 32 字节时密文变成 48 字节。如果长度出现“非 16 的倍数”的结果说明填充逻辑出问题了。第二段 SQL 看两次加密是否一致如果不一致说明你的实现里带了随机 iv这是正常现象但你要知道这一点否则在数据比对时会以为加密结果不稳定。参数说明这块验证特别重要。如果等保测评要求“加密后密文不可预测”那随机 iv 是加分项如果业务要求“同一输入能精确匹配查询”那必须固定 iv。两种需求冲突时用“确定性加密 冗余明文列”的组合最常见。5.3 数据库双写方案加密改造期间的数据一致性检查实际项目里经常要运行一套“明文 密文”双写过渡方案。新数据同时加密写入到期后一次性切换。这个阶段的比对逻辑简单但很重要-- 双写检查解密后必须等于明文列 SELECT COUNT(*) AS mismatch_cnt FROM t_user_bak WHERE pkg_crypto_util.fn_decrypt_raw(mobile_cipher) mobile_plain;逻辑说明t_user_bak 是过渡期表同一数据同时存明文和密文。定期跑这条 SQL检查解密结果和明文列是否一致不一致就说明加密写入出问题了趁数据量小赶紧修。等保测评的资料里也可以直接附上这份检查报告作为“加密功能有效性的证据材料”。参数说明双写表只用于过渡期最多保留一个月。切换时先停写入等存量数据全部解密校验通过后再更新应用连接串指向新表结构旧表归档。这里注意归档表上的数据仍然是密文无权访问的人看不到明文归档过程中不要顺手把密文表导出到测试环境避免扩散。最后说一个我自己的习惯任何加密函数上线前我一定先做一次“意外断电模拟”。杀掉会话后重启数据库再看一遍解密能不能正常执行。DBMS_CRYPTO 的包状态在实例恢复后偶尔会失效提前验证过才不会在生产切换时手忙脚乱。另外密钥文件、包源码、测试脚本要一并放进版本管理别只存在某个 DBA 的本地目录里。这套东西不难但每一步都按规矩走等保测评的数据安全控制项基本能顺利通过。希望帮到你。本文还有配套的精品资源点击获取