SIM卡的ICCID校验位的计算方法,应该不是什么隐私或者机密吧,最近关于这个有点小发现,故写出来与大家分享一下。
ICCID (Integerate Circuit Card Identity),集成电路卡识别码,是SIM卡的唯一识别号码。ICCID长为19~20位,其中最后一位是校验位(当然也有另外,中移动的某些卡就不是校验位,而是普通的序列号)。关于其它位的含义,可自行百度。
最近要处理中国联通物联网卡的相关业务,需要对接联通的Jasper平台,封装成自己的一套API给客户用,查询卡的信息是通过卡的ICCID(移动、电信用的是MSISDN)。同事给过来一张联通的卡号清单,有ICCID和MSISDN,通过API查询其中一个ICCID的相关信息,结果没查到,于是通过浏览器在联通的管理平台再次查询,还是没查到,这就纳闷了!询问同事,同事说要在ICCID后面加上*
,才能查询到。测试了一下,果然可以查到,只是查出的号码多了一位!
原来,同事给我的号码清单,其中ICCID只有19位,而通过API或者管理平台,需要20位的ICCID才能查询,其中第二十位就是前面19位的校验位。
现在问题来了:如何计算ICCID的校验位?
首先是百度,通过“iccid 校验位”关键字查询,很多人问与我同样的问题,大部分介绍的都是Luhn
算法,其中知乎上有位热心的网友给出了详细的关于ICCID的介绍,以及Luhn
算法的维基百科链接。进入该维基百科的页面,都是些英文,后面是各种语言的Luhn
算法实现,由于目前是用PHP开发,就把PHP版本的代码复制了下来:
function checkLuhn($number) {
$sum = 0;
$numDigits = strlen($number)-1;
$parity = $numDigits % 2;
for ($i = $numDigits; $i >= 0; $i--) {
$digit = substr($number, $i, 1);
if (!$parity == ($i % 2)) {$digit <<= 1;}
$digit = ($digit > 9) ? ($digit - 9) : $digit;
$sum += $digit;
}
return (0 == ($sum % 10));
}
由于是求校验位,显然,return
那行代码不对,我们只需要求校验和除10的余数就行了。如下:
function checkLuhn($number) {
$sum = 0;
$numDigits = strlen($number)-1;
$parity = $numDigits % 2;
for ($i = $numDigits; $i >= 0; $i--) {
$digit = substr($number, $i, 1);
if (!$parity == ($i % 2)) {$digit <<= 1;}
$digit = ($digit > 9) ? ($digit - 9) : $digit;
$sum += $digit;
}
return ($sum % 10);
}
通过同事告诉我的方法,我在联通管理后台查询到了几个ICCID的校验位,然后对比用Luhn
算法的计算结果,如下:
ICCID(中间号码用* 代替) |
实际校验位 | Luhn算法计算的校验位 |
---|---|---|
8986***68250 | 3 | 0 |
8986***68251 | 1 | 1 |
8986***68252 | 9 | 2 |
显然,用这个Luhn
算法计算出来的校验位不对,可能的原因有二:
Luhn
算法上做了改进对于第一种情况,一阵搜索之后,没有发现其他可能的ICCID校验位计算方法,于是我把焦点放在情况2。
因为可以通过19位ICCID逐一查到对应的校验位,现在的问题转化为:
已知19位ICCID及其对应的校验位,求它们之间函数关系
收集了尾号8250~8259共10个ICCID的实际校验位与用Luhn
算法计算的校验位,如下:
ICCID(中间号码用* 代替) |
实际校验位 | Luhn算法计算的校验位 |
---|---|---|
8986***68250 | 3 | 0 |
8986***68251 | 1 | 1 |
8986***68252 | 9 | 2 |
8986***68253 | 7 | 3 |
8986***68254 | 5 | 4 |
8986***68255 | 2 | 5 |
8986***68256 | 0 | 6 |
8986***68257 | 8 | 7 |
8986***68258 | 6 | 8 |
8986***68259 | 4 | 9 |
从以上表格的数据可以看出,当ICCID的第19位从0~9变化时,实际校验位呈现一种没有明显规律的变化,时大时小。不过细心察看,还是可以总结出2个规律:
- 实际校验位处于0~9之间,都是除10的余数,没有出现字母。对比一下
Luhn
算法的代码,猜想这跟前面第一部分我们讲到的Luhn
算法有些关系。- 相邻的校验位之间相差2(由于猜想程序的最后一步,校验位是通过除10的余数计算出来的,所以,1与9可以看作11和9,这样也是相差2)。
为了形象一点,将ICCID第19位与校验位的关系做成图表,如下:
到目前为止,ICCID第19位与校验位的关系还算是有点眉目,不过ICCID所有位与校验位的关系还是没法研究。我们需要更多的数据。
这次我把焦点放在ICCID的第18位上。
收集了尾号8260~8269共10个ICCID的实际校验位,与尾号8250~8259的对比,如下:
ICCID(中间号码用* 代替) |
实际校验位 | ICCID(中间号码用* 代替) |
实际校验位 |
---|---|---|---|
8986***68250 | 3 | 8986***68260 | 2 |
8986***68251 | 1 | 8986***68261 | 0 |
8986***68252 | 9 | 8986***68262 | 8 |
8986***68253 | 7 | 8986***68263 | 6 |
8986***68254 | 5 | 8986***68264 | 4 |
8986***68255 | 2 | 8986***68265 | 1 |
8986***68256 | 0 | 8986***68266 | 9 |
8986***68257 | 8 | 8986***68267 | 7 |
8986***68258 | 6 | 8986***68268 | 5 |
8986***68259 | 4 | 8986***68269 | 3 |
这次有重大规律了,尾号8260~8269的一系列卡号的校验位,比尾号8250~8259的校验位小1,即:
两个ICCID,如果只有第18位数字不同,第18位数字大1,那么其校验位要小1。(仍然是基于校验位的最后一步是通过除10的余数计算出来的这个假设,所以,0与9可以看作10和9,也是相差1。)
*如8986
***
682**5**0的校验位为3,而8986***
682**6**0的校验位为2。
再研究下ICCID第17位,与上面表格中的部分数据对比,如下。
ICCID(中间号码用* 代替) |
实际校验位 | ICCID(中间号码用* 代替) |
实际校验位 |
---|---|---|---|
8986***68350 | 1 | 8986***68250 | 3 |
8986***68351 | 9 | 8986***68251 | 1 |
这次只有第17位不一样,校验位相差2。
总结以上规律,以及对比Luhn
算法:
- 两个ICCID,仅第18位不一样,校验位相差1;仅第17位不一样,校验位相差2,应该是这行代码的结果:
if (!$parity == ($i % 2)) {$digit <<= 1;}
第18位数字越大,其校验位越小(相对而言,仅比较第18位为6和5的情况),这行代码
$digit = ($digit > 9) ? ($digit - 9) : $digit;
可改为:
$digit = ($digit > 9) ? ($digit - 9) : (9 - $digit);
修正后的Luhn
算法如下:
function checkLuhn($number) {
$sum = 0;
$numDigits = strlen($number)-1;
$parity = $numDigits % 2;
for ($i = $numDigits; $i >= 0; $i--) {
$digit = substr($number, $i, 1);
if (!$parity == ($i % 2)) {$digit <<= 1;}
$digit = ($digit > 9) ? ($digit - 9) : (9 - $digit);
$sum += $digit;
}
return $sum % 10;
}
用改进后的Luhn
算法对尾号为8250、8260、8350的三个19位ICCID计算校验位,对比真实校验位,如下:
ICCID(中间号码用* 代替) |
实际校验位 | Luhn算法计算的校验位 |
---|---|---|
8986***68250 | 3 | 4 |
8986***68260 | 2 | 6 |
8986***68350 | 1 | 3 |
没看出有什么规律,也无法比较我们自己算出的校验位与实际校验位的区别。
考虑到我们在研究”ICCID的第19位与校验位的关系”时的发现,第19位看似有特殊规律。不妨把第19位去掉,只对前18位计算校验位试下。结果如下:
ICCID(去掉第19位,中间号码用* 代替) |
实际校验位 | Luhn算法计算的校验位 |
---|---|---|
8986***6825 | 3 | 2 |
8986***6826 | 2 | 1 |
8986***6835 | 1 | 0 |
这次有明显规律了,我们的计算结果与实际结果相差1。
为了补偿这个差别,我们只需在return
那行代码前加上一句:
$sum += 1;
ICCID的前18位与校验位的关系差不多理清楚了,接下来处理第19位。
由于时间比较紧,没那么多时间去研究了。我打算用表格方式来表达ICCID的第19位与校验位的关系,在PHP程序中就是用数组了,初始化一个数组:
$arr = [
3,1,9,7,5,2,0,8,6,4
];
由于ICCID的前18位与校验位的关系已初步确定,我们猜测,19位ICCID的校验位为前18位的Luhn
校验位与第19位通过某种方式计算出来的校验位相加而成,然后除10取余数,于是把对总体校验位的计算封装了这个函数:
function checkCode($number) {
global $arr;
$sum = checkLuhn(substr($number, 0, 18));
return ($sum + $arr[$number[18]])%10;
}
依然是对上述三个ICCID计算校验位,如下:
ICCID(中间号码用* 代替) |
实际校验位 | Luhn算法计算的校验位 |
---|---|---|
8986***68250 | 3 | 6 |
8986***68260 | 2 | 5 |
8986***68350 | 1 | 4 |
现在我们的计算结果与实际相差3,我们可以把每个结果减去3,不过为了避免负数,还是加上7比较好。
所以,checkCode
这个函数可修正为:
function checkCode($number) {
global $arr;
$sum = checkLuhn(substr($number, 0, 18));
return ($sum + $arr[$number[18]] + 7)%10;
}
用该方法计算的校验位,附加到19位ICCID之后组成20位ICCID,对2000多个ICCID计算,然后与从联通管理后台到处的20位ICCID比较,完全吻合!说明这个算法是正确的,至少对于这2000多个ICCID是正确的。
整合后的代码为:
$arr = [
3,1,9,7,5,2,0,8,6,4
];
function checkLuhn($number) {
$sum = 0;
$numDigits = strlen($number)-1;
$parity = $numDigits % 2;
for ($i = $numDigits; $i >= 0; $i--) {
$digit = substr($number, $i, 1);
if (!$parity == ($i % 2)) {$digit <<= 1;}
$digit = ($digit > 9) ? ($digit - 9) : (9 - $digit);
$sum += $digit;
}
$sum += 1;
return $sum % 10;
}
function checkCode($number) {
global $arr;
$sum = checkLuhn(substr($number, 0, 18));
return ($sum + $arr[$number[18]] + 7)%10;
}
问题是解决了。不过感觉以上代码不够优雅。
还是看这个图:
由于校验位最终是通过对10取模得到的,因此我们可以对校验位加上10的倍数,也不会影响到最终结果。按照这个思路,把上图中的校验位适当加上10的整数倍,构成递增序列,画图如下:
从上图中,我们可以看到,ICCID第19位与扩充后的校验位成明显的线性关系。
实际上,对数据进行归纳后发现,扩充后的校验位与ICCID第19位有如下简单的函数关系:
当x<5时, y = 8*x + 3
当x>=5时, y = 8*x + 2
这时我们可以把那个辅助数组给去掉了,用函数解析式来表达更优雅一些。
最后,把我们的发现整合这几行代码:
$sum += 1;
与
return ($sum + $arr[$number[18]] + 7)%10;
我们有一个比较优雅的计算方法了,称为checkLuhn2
function checkLuhn2($number) {
$sum = 0;
$numDigits = strlen($number)-1;
$numDigits -= 1;
$parity = $numDigits % 2;
for ($i = $numDigits; $i >= 0; $i--) {
$digit = substr($number, $i, 1);
if (!$parity == ($i % 2)) {$digit <<= 1;}
$digit = ($digit > 9) ? ($digit - 9) : (9 - $digit);
$sum += $digit;
}
$digit = $number[$numDigits+1];
$sum += ($digit < 5 ? $digit*8+1 : $digit*8);
return $sum % 10;
}
目前来说,对于这2000多张卡,ICCID校验和的计算结果是OK的。如果遇到更多的卡,还需验证下,毕竟这个计算方法只是个人研究出来的(基于现有的Luhn
算法)。
对于这种没有明确处理思路的问题,关键是找准切入点,然后一步一步逼近目标。对于暂时处理不了的部分,可暂时搁置在一边,等时问题解决得差不多再来处理。
这个算法能够研究出来,除了本人的”东拼西凑”(各种类似正交试验的数据对比,以及各种试探之外),还得幸亏联通是用了修正后的Luhn
算法,不然没这么容易。