背景
现有一个个人信用积分管理和应用平台,为实现监管方对用户数据的监管以及用户方的数据认证,需要对一些字段内容进行编码,然后进行CL加密,最后将其上链(存放在区块链上),要求监管方可对编码的信息进行解码还原出原本的信息内容。
本文着眼于用户数据中的“身份信息”字段,包括姓名与身份证号两项内容,将其编码成字符串后进行拼接,得到供后续加密乃至上链所需的字符串,并能够解码还原原本信息。提供两种思路,以适应不同加密输入需求。
思路一•编码方案
姓名
-
编码规则:采取utf-8编码规则,将汉字转化为对应的十六进制字符串
UTF-8是针对Unicode的一种可变长度字符长度编码,创建于1992年。它可以用来表示Unicode标准中的任何字符,Unicode是为了解决传统的字符编码方案的局限而产生的,它为每种语言中的每个字符设定了统一并且唯一的二进制编码,以满足跨语言、跨平台进行文本转换、处理的要求。它可以使用1~4个字节表示一个符号,根据不同的符号而变化字节长度,因此可以节省存储空间。它对英文使用8位(一个字节),中文使用24位(三个字节)来编码。
-
代码实现:
定义变量
encoded_name
为将汉字对应的utf-8字符串转为对应的十六进制字符串let encoded_name = utf8_to_hex(&info.name)?;
定义函数
utf8_to_hex
的功能为:将字符串转换为字节数组,创建一个字节数组的迭代器,并对每个字节进行格式化,转换为两位的十六进制字符串,最后将所有格式化后的字符串片段收集并连接成一个完整的字符串fn utf8_to_hex(utf8_str: &str) -> String { utf8_str.as_bytes().iter().map(|byte| format!("{:02x}", byte)).collect() }
身份证号
-
编码规则:规定用0-9,a-z来表示的三十六进制用来压缩身份证,将其从18位压缩到10位。1
原理:两个36进制字符可以最大表示3636-1=1295,而1295>999,因此可以用两个36进制字符替代身份证号码中的3个数字字符。例如:36进制的4A=436+10=154(十进制)。
接下来我们详细解释,编码流程如下。
以虚拟身份证号码88866620051027420X为例:
- 号码位1,2,3,对应原始字符为888,编码方式为用两位36进制字符替换,编码结果为OO
- 号码位4,5,6,对应原始字符为666,编码方式为用两位36进制字符替换,编码结果为II
- 号码位7,8,11,12(出生年份前两位和月份),对应原始字符为2010,编码方式为用0-B表示18**年1-12月,C-N表示1 9**年1-12月,O-Z表示20**年1-12月(因为36进制恰好是12的3倍),编码结果为X
- 号码位13,14(出生日期),对应原始字符为27,编码方式为用一位36进制字符替换,编码结果为R
- 号码位9,10,17(出生年份后两位和性别识别位),对应原始字符为050,编码方式为用两位36进制字符替换,编码结果为1E
- 号码位18,15,16,对应原始字符为X42,编码方式为如果18位是X,就用10代替,X42变为1042,再用两位36进制字符替换。(18位前置是为了避免在X变为10的情况下组合成的数字大于1295,在此例中,如果不前置,组合成的数字就是4210,超过了两位36进制字符可以表示的范围)编码结果为SY
至此,编码完毕,88866620051027420X编码为OOIIXR1ESY
-
代码实现:
规定三十六进制的表示方法
fn encode_char(value: usize) -> Result<char, String> { const CHARS: &str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; CHARS.chars().nth(value).ok_or_else(|| "编码值超出范围".to_string()) }
定义压缩函数以及返回类型
fn compress_id(id: &str) -> Result<String, String>{...}
判断长度是否为18位
if id.len() != 18 { return Err("身份证号长度必须为18位".to_string()); }
创建一个可变的空字符串
encoded_id
,用于存储编码后的 IDlet mut encoded_id = String::new();
1,2,3位编码
//从 ID 的前3个字符中提取一个子字符串,并将其解析为一个无符号的整数 (usize) let value1 = id[0..3].parse::<usize>().map_err(|_| "解析错误")?; //将解析得到的value1除以36,并将结果传递给encode_char函数进行编码。编码结果被追加到encoded_id字符串中 encoded_id.push(encode_char(value1 / 36)?); //将value1对36取模,并将结果传递给encode_char函数进行编码。编码结果被追加到encoded_id字符串中 encoded_id.push(encode_char(value1 % 36)?);
4,5,6位编码
let value2 = id[3..6].parse::<usize>().map_err(|_| "解析错误")?; encoded_id.push(encode_char(value2 / 36)?); encoded_id.push(encode_char(value2 % 36)?);
7,8,11,12位编码
let year_prefix = id[6..8].parse::<usize>().map_err(|_| "解析错误")?; let month = id[10..12].parse::<usize>().map_err(|_| "解析错误")?; let year_month_code = if year_prefix == 19 { 12 + month - 1 //用中12位表示20世纪1-12月 } else if year_prefix == 20 { 24 + month - 1 //用后12位表示21世纪1-12月 } else { month - 1 //用前12位表示19世纪1-12月 }; encoded_id.push(encode_char(year_month_code)?);
13,14位编码
let day = id[12..14].parse::<usize>().map_err(|_| "解析错误")?; encoded_id.push(encode_char(day)?);
9,10,17位编码
let year_suffix = id[8..10].parse::<usize>().map_err(|_| "解析错误")?; let gender = id[16..17].parse::<usize>().map_err(|_| "解析错误")?; let year_gender_code = year_suffix * 10 + gender;//将9,10,17位组合在一起 encoded_id.push(encode_char(year_gender_code / 36)?); encoded_id.push(encode_char(year_gender_code % 36)?);
18,15,17位编码
//第18位若是"X",则将last_char设为10。否则将其解析为一个无符号整数 let last_char = if &id[17..18] == "X" { 10 } else { id[17..18].parse::<usize>().map_err(|_| "解析错误")? }; let last_part = last_char * 100 + id[14..16].parse::<usize>().map_err(|_| "解析错误")?; //将18,15,17位组合起来 encoded_id.push(encode_char(last_part / 36)?); encoded_id.push(encode_char(last_part % 36)?);
返回编码后的
encoded_id
字符串Ok(encoded_id)
测试用例
-
test代码:
#[test] fn test_compress_id() { let info = IDInfo { name: String::from("吴祥生"), id: String::from("52102320041123884X"), }; match encode_personal_info(&info) { Ok(encoded) => println!("Encoded info: {}", encoded), Err(e) => eprintln!("Error encoding info: {}", e), } }
-
运行结果:
---- id_encode::test_compress_id stdout ---- Encoded info: e590b4e7a5a5e7949fEH0NYN18U8
思路一•解码方案
-
代码实现:
规定三十六进制的表示方法
fn decode_char(c: char) -> Result<usize, DecodeError> { const CHARS: &str = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"; CHARS.find(c).ok_or_else(|| DecodeError(format!("解码字符超出范围: {}", c))) }
定义函数
hex_to_utf8
用于将十六进制还原为utf-8字符串fn hex_to_utf8(hex_str: &str) -> Result<String, DecodeError> { //通过映射函数和collect方法将十六进制字符串转换为字节向量 let bytes: Result<Vec<u8>, _> = (0..hex_str.len()) .step_by(2) .map(|i| u8::from_str_radix(&hex_str[i..i + 2], 16)) .collect(); //根据转换结果来处理错误或者返回UTF-8字符串 match bytes { Ok(bytes) => String::from_utf8(bytes).map_err(|_| DecodeError("UTF-8 解码错误".to_string())), Err(_) => Err(DecodeError("解析十六进制字符串错误".to_string())), } }
定义身份证解码函数(基本上是编码部分的逆运算,不多做解释)
fn decompress_id(compressed_id: &str) -> Result<String, DecodeError> { if compressed_id.len() != 10 { return Err(DecodeError("压缩ID长度必须为10个字符".to_string())); } let mut id = String::new(); let value1 = decode_char(compressed_id.chars().nth(0).unwrap())? * 36 + decode_char(compressed_id.chars().nth(1).unwrap())?; id.push_str(&format!("{:03}", value1)); let value2 = decode_char(compressed_id.chars().nth(2).unwrap())? * 36 + decode_char(compressed_id.chars().nth(3).unwrap())?; id.push_str(&format!("{:03}", value2)); let year_month_code = decode_char(compressed_id.chars().nth(4).unwrap())?; let (year_prefix, month) = if year_month_code >= 24 { (20, year_month_code - 24 + 1) } else if year_month_code >= 12 { (19, year_month_code - 12 + 1) } else { (0, year_month_code + 1) }; id.push_str(&format!("{:02}", year_prefix)); let day = decode_char(compressed_id.chars().nth(5).unwrap())?; let year_suffix = decode_char(compressed_id.chars().nth(6).unwrap())? * 36 + decode_char(compressed_id.chars().nth(7).unwrap())?; let gender = year_suffix % 10; let year_suffix = year_suffix / 10; id.push_str(&format!("{:02}", year_suffix)); id.push_str(&format!("{:02}", month)); id.push_str(&format!("{:02}", day)); let last_part = decode_char(compressed_id.chars().nth(8).unwrap())? * 36 + decode_char(compressed_id.chars().nth(9).unwrap())?; let last_char = last_part / 100; let remaining = last_part % 100; id.push_str(&format!("{:02}", remaining)); id.push_str(&format!("{}", gender)); if last_char == 10 { id.push('X'); } else { id.push_str(&format!("{}", last_char)); } Ok(id) }
定义函数将编码组合信息分离成身份证号编码与姓名编码部分,并分别调用前文单独的解码函数,最终返回解码后的姓名和身份证信息
pub fn decode_personal_info(encoded_str: &str) -> Result<IDInfo, DecodeError> { const COMPRESSED_ID_LENGTH: usize = 10; // 计算姓名部分的十六进制字符串长度(因为姓名编码长度不定,所以用总长度减去身份证编码的长度) let name_hex_length = encoded_str.len() - COMPRESSED_ID_LENGTH; // 检查姓名部分的长度是否符合要求(规定姓名在2~6个汉字间,可自行定义,因为utf-8下一个汉字占三个字节,一个字节需要2个十六进制字符表示,故姓名编码是在12~36个十六进制字符范围内且为6的倍数) if name_hex_length < 12 || name_hex_length > 36 || name_hex_length % 6 != 0 { return Err(DecodeError("姓名部分编码长度不正确".to_string())); } // 提取姓名部分的十六进制字符串 let name_hex = &encoded_str[..name_hex_length]; // 将姓名部分的十六进制字符串解码为UTF-8字符串 let name = hex_to_utf8(name_hex)?; // 提取压缩的ID部分 let compressed_id = &encoded_str[name_hex_length..]; // 解压缩ID部分 let id = decompress_id(compressed_id)?; // 返回解码后的姓名和ID信息 Ok(IDInfo { name, id }) }
-
test用例:
#[test] fn test_decode_id() { let encoded_str = "e590b4e7a5a5e7949fEH0NYN18U8"; // 示例编码字符串 match decode_personal_info(&encoded_str) { Ok(info) => println!("Decoded info: Name: {}, ID: {}", info.name, info.id), Err(e) => eprintln!("Error decoding info: {}", e), } }
-
运行结果:
---- id_decode::test_decode_id stdout ---- Decoded info: Name: 吴祥生, ID: 52102320041123884X
思路二•编码方案
姓名
-
编码规则:采取utf-8编码规则,将汉字转化为对应的十六进制字符串后,继续将十六进制字符串转换为对应的十进制数。这样做的目的是为了满足某些加密系统的输入只能是数字不能出现字母的要求。
-
代码实现:
定义变量
encoded_name
为将汉字对应的utf-8字符串转为对应的十六进制字符串let encoded_name = utf8_to_dec(&info.name)?;
定义函数
utf8_to_dec
将UTF-8字符串转换为一个十进制表示的字符串,这里解释下十六进制转换为十进制:先将十六进制字符串转换为一个u128
类型的整数,如果转换失败,返回自定义的错误信息如果转换失败,返回自定义的错误信息,再将u128
类型的整数转换为十进制字符串fn utf8_to_dec(utf8_str: &str) -> Result<String, String> { // 将UTF-8字符串转换为十六进制字符串,同思路一,不再赘述 let hex_str: String = utf8_str .as_bytes() .iter() .map(|byte| format!("{:02x}", byte)) .collect(); // 将十六进制字符串转换为十进制字符串 let dec_str = u128::from_str_radix(&hex_str, 16) .map_err(|_| "将16进制转为十进制失败".to_string())? .to_string(); // 返回十进制字符串 Ok(dec_str) }
身份证号
-
编码规则:若身份证全是数字则不变,若最后一位为X则将X变为000000加在末尾。这同样是为了适应输入要求全为数字的系统。
解释一下为何要将X变为000000:在解码时拿到一个字符串,首先要确定身份证号编码的位数,在思路一中由于身份证固定被转为10位,所以用总长度-10就能得到姓名编码长度。而思路二中当X转换后位数与原本位数不同(有人说那直接将X转换为0不就行了,这样做的问题是极端情况下可能会与另一身份证号重复),相当于姓名身份证二者编码长度都不定,所以我们得先确定其中一个的长度,姓名不好确定(因为你解码前不可能知道姓名有几个汉字),还得从身份证号入手,这样又引入新的问题,我将X转换为
00
、000
、0000
可以吗?为什么偏偏是六个0?首先,由于身份证后五位(14-18位)可能出现任何数字,因而若将X变为5位以下的一个数,则小概率可能会造成与另一末尾不为X的身份证号最后五位重复,这样我们在恢复身份证号时,可能会将原本正常的身份证号识别为末尾为X的身份证号,从而产生错误。身份证第13、14位,即倒数五六位,代表出生日期,不可能同时为0,故转换为六个0不会出现与正常身份证号重复的情况 -
代码实现:
定义压缩函数及返回类型
fn encode_id(id: &str) -> Result<String, String> {...}
判断是否为18位
if id.len() != 18 { return Err("身份证号长度必须为18位".to_string()); }
提取前17位字符,并将其转换为字符串
let mut encoded_id = id[..17].to_string();
处理最后一位,若为X,替换为
000000
,否则保持原样let last_char = if &id[17..18] == "X" { "000000".to_string() // 如果第18位是X,替换为000000 } else { id[17..18].to_string() // 否则保持原样 };
将处理后的第18位字符添加到编码后的字符串中并返回
encoded_id.push_str(&last_char); Ok(encoded_id)
测试用例
-
test代码:
#[test] fn test_compress_id() { let info = IDInfo { name: String::from("吴祥生"), id: String::from("52102320041123884X"), }; match encode_personal_info(&info) { Ok(encoded) => println!("Encoded info: {}", encoded), Err(e) => eprintln!("Error encoding info: {}", e), } }
-
运行结果:
---- id_encode::test_compress_id stdout ---- Encoded info: 423473160661539589852752102320041123884000000
思路二•解码方案
-
代码实现:
定义函数将十六进制字符串转换utf-8字符串
fn hex_to_utf8(hex_str: &str) -> Result<String, DecodeError> { let bytes: Result<Vec<u8>, _> = (0..hex_str.len()) .step_by(2) //生成一个范围,步长为2,以便每次处理两个十六进制字符 .map(|i| u8::from_str_radix(&hex_str[i..i + 2], 16)) .collect(); match bytes { Ok(bytes) => { String::from_utf8(bytes).map_err(|_| DecodeError("UTF-8 解码错误".to_string())) } Err(_) => Err(DecodeError("解析十六进制字符串错误".to_string())), } }
定义函数将十进制数转换为对应十六进制
fn dec_to_hex(dec_str: &str) -> Result<String, DecodeError> { let dec_num = u128::from_str_radix(dec_str, 10) .map_err(|_| DecodeError("将十进制转为数字失败".to_string()))?; Ok(format!("{:x}", dec_num)) }
定义函数
decode_id
将编码的身份证号解码为原始身份证号fn decode_id(encoded_id: &str) -> Result<String, DecodeError> { // 获取编码后的身份证号长度 let id_len = encoded_id.len(); // 提取身份证号不包括最后6个字符的部分 let id = &encoded_id[..id_len - 6]; // 提取身份证号的最后6个字符 let last_six = &encoded_id[id_len - 6..]; // 处理最后6个字符 let last_char = if last_six == "000000" { "X".to_string() // 如果最后6个字符是 "000000",替换为 "X" } else { last_six.to_string() // 否则保持原样 }; // 将前部分和处理后的最后一个字符连接起来,返回解码后的身份证号 Ok(id.to_string() + &last_char) }
定义函数
decode_personal_info
将编码的字符串解码为包含姓名和身份证号的结构体IDInfo
pub fn decode_personal_info(encoded_str: &str) -> Result<IDInfo, DecodeError> { // 确定编码后的身份证号长度,若最后6位为000000,则将身份证号编码部分长度定为23位,否则18位 let encoded_id_len = if encoded_str.ends_with("000000") { 23 } else { 18 }; // 姓名部分的长度 let name_dec_length = encoded_str.len() - encoded_id_len; // 提取姓名的十进制编码部分 let name_dec = &encoded_str[..name_dec_length]; // 将十进制编码的姓名转换为十六进制 let name_hex = dec_to_hex(name_dec)?; // 将十六进制编码的姓名转换为UTF-8字符串 let name = hex_to_utf8(&name_hex)?; // 提取编码后的身份证号部分 let encoded_id = &encoded_str[name_dec_length..]; // 解码身份证号 let id = decode_id(encoded_id)?; // 返回包含姓名和身份证号的结构体 Ok(IDInfo { name, id }) }
-
test用例:
#[test] fn test_decode_id() { let encoded_str = "423473160661539589852752102320041123884000000"; // 示例编码字符串 // e6aca7e998b3e5b09aEH0NYN15P0 match decode_personal_info(&encoded_str) { Ok(info) => println!("Decoded info: Name: {}, ID: {}", info.name, info.id,), Err(e) => eprintln!("Error decoding info: {}", e), } }
-
运行结果:
---- id_decode::test_decode_id stdout ---- Decoded info: Name: 吴祥生, ID: 52102320041123884X
总结
视不同需求,可以对编码算法继续进行改进,如果有什么问题,欢迎讨论。
注:该方法参考了文章一种身份证号码的编码压缩方法 ↩︎