联通物联网卡ICCID校验位的计算方法探究

SIM卡的ICCID校验位的计算方法,应该不是什么隐私或者机密吧,最近关于这个有点小发现,故写出来与大家分享一下。

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及其对应的校验位,求它们之间函数关系

第二部分: 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个规律:

  1. 实际校验位处于0~9之间,都是除10的余数,没有出现字母。对比一下Luhn算法的代码,猜想这跟前面第一部分我们讲到的Luhn算法有些关系。
  2. 相邻的校验位之间相差2(由于猜想程序的最后一步,校验位是通过除10的余数计算出来的,所以,1与9可以看作11和9,这样也是相差2)。

为了形象一点,将ICCID第19位与校验位的关系做成图表,如下:

联通物联网卡ICCID校验位的计算方法探究_第1张图片

到目前为止,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;
}

第三部分:再次探究ICCID校验位的计算方法

问题是解决了。不过感觉以上代码不够优雅。

第19位ICCID与校验位的关系

还是看这个图:

联通物联网卡ICCID校验位的计算方法探究_第2张图片

由于校验位最终是通过对10取模得到的,因此我们可以对校验位加上10的倍数,也不会影响到最终结果。按照这个思路,把上图中的校验位适当加上10的整数倍,构成递增序列,画图如下:

联通物联网卡ICCID校验位的计算方法探究_第3张图片

从上图中,我们可以看到,ICCID第19位与扩充后的校验位成明显的线性关系。

实际上,对数据进行归纳后发现,扩充后的校验位与ICCID第19位有如下简单的函数关系:

x<5时,  y = 8*x + 3x>=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算法,不然没这么容易。

你可能感兴趣的:(其它)