1# meid.py - functions for handling Mobile Equipment Identifiers (MEIDs) 2# 3# Copyright (C) 2010-2017 Arthur de Jong 4# 5# This library is free software; you can redistribute it and/or 6# modify it under the terms of the GNU Lesser General Public 7# License as published by the Free Software Foundation; either 8# version 2.1 of the License, or (at your option) any later version. 9# 10# This library is distributed in the hope that it will be useful, 11# but WITHOUT ANY WARRANTY; without even the implied warranty of 12# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 13# Lesser General Public License for more details. 14# 15# You should have received a copy of the GNU Lesser General Public 16# License along with this library; if not, write to the Free Software 17# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 18# 02110-1301 USA 19 20"""MEID (Mobile Equipment Identifier). 21 22The Mobile Equipment Identifier is used to identify a physical piece of 23CDMA mobile station equipment. 24 25>>> validate('AF 01 23 45 0A BC DE C') 26'AF0123450ABCDE' 27>>> validate('29360 87365 0070 3710 0') 28'AF0123450ABCDE' 29>>> validate('29360 87365 0070 3710 0', strip_check_digit=False) 30'AF0123450ABCDEC' 31>>> validate('29360 87365 0070 3710 1') 32Traceback (most recent call last): 33 ... 34InvalidChecksum: ... 35>>> format('af0123450abcDEC', add_check_digit=True) 36'AF 01 23 45 0A BC DE C' 37>>> format('af0123450abcDEC', format='dec', add_check_digit=True) 38'29360 87365 0070 3710 0' 39""" 40 41from stdnum.exceptions import * 42from stdnum.util import clean, isdigits 43 44 45_hex_alphabet = '0123456789ABCDEF' 46 47 48def _cleanup(number): 49 """Remove any grouping information from the number and removes surrounding 50 whitespace.""" 51 return clean(number, ' -').strip().upper() 52 53 54def _ishex(number): 55 for x in number: 56 if x not in _hex_alphabet: 57 return False 58 return True 59 60 61def _parse(number): 62 number = _cleanup(number) 63 if len(number) in (14, 15): 64 # 14 or 15 digit hex representation 65 if not _ishex(number): 66 raise InvalidFormat() 67 return number[0:14], number[14:] 68 elif len(number) in (18, 19): 69 # 18-digit decimal representation 70 if not isdigits(number): 71 raise InvalidFormat() 72 return number[0:18], number[18:] 73 else: 74 raise InvalidLength() 75 76 77def calc_check_digit(number): 78 """Calculate the check digit for the number. The number should not 79 already have a check digit.""" 80 # both the 18-digit decimal format and the 14-digit hex format 81 # containing only decimal digits should use the decimal Luhn check 82 from stdnum import luhn 83 if isdigits(number): 84 return luhn.calc_check_digit(number) 85 else: 86 return luhn.calc_check_digit(number, alphabet=_hex_alphabet) 87 88 89def compact(number, strip_check_digit=True): 90 """Convert the MEID number to the minimal (hexadecimal) representation. 91 This strips grouping information, removes surrounding whitespace and 92 converts to hexadecimal if needed. If the check digit is to be preserved 93 and conversion is done a new check digit is recalculated.""" 94 # first parse the number 95 number, cd = _parse(number) 96 # strip check digit if needed 97 if strip_check_digit: 98 cd = '' 99 # convert to hex if needed 100 if len(number) == 18: 101 number = '%08X%06X' % (int(number[0:10]), int(number[10:18])) 102 if cd: 103 cd = calc_check_digit(number) 104 # put parts back together again 105 return number + cd 106 107 108def _bit_length(n): 109 """Return the number of bits necessary to store the number in binary.""" 110 try: 111 return n.bit_length() 112 except AttributeError: # pragma: no cover (Python 2.6 only) 113 import math 114 return int(math.log(n, 2)) + 1 115 116 117def validate(number, strip_check_digit=True): 118 """Check if the number is a valid MEID number. This converts the 119 representation format of the number (if it is decimal it is not converted 120 to hexadecimal).""" 121 from stdnum import luhn 122 # first parse the number 123 number, cd = _parse(number) 124 if len(number) == 18: 125 # decimal format can be easily determined 126 if cd: 127 luhn.validate(number + cd) 128 # convert to hex 129 manufacturer_code = int(number[0:10]) 130 serial_num = int(number[10:18]) 131 if _bit_length(manufacturer_code) > 32 or _bit_length(serial_num) > 24: 132 raise InvalidComponent() 133 number = '%08X%06X' % (manufacturer_code, serial_num) 134 cd = calc_check_digit(number) 135 elif isdigits(number): 136 # if the remaining hex format is fully decimal it is an IMEI number 137 from stdnum import imei 138 imei.validate(number + cd) 139 else: 140 # normal hex Luhn validation 141 if cd: 142 luhn.validate(number + cd, alphabet=_hex_alphabet) 143 if strip_check_digit: 144 cd = '' 145 return number + cd 146 147 148def is_valid(number): 149 """Check if the number is a valid MEID number.""" 150 try: 151 return bool(validate(number)) 152 except ValidationError: 153 return False 154 155 156def format(number, separator=' ', format=None, add_check_digit=False): 157 """Reformat the number to the standard presentation format. The separator 158 used can be provided. If the format is specified (either 'hex' or 'dec') 159 the number is reformatted in that format, otherwise the current 160 representation is kept. If add_check_digit is True a check digit will be 161 added if it is not present yet.""" 162 # first parse the number 163 number, cd = _parse(number) 164 # format conversions if needed 165 if format == 'dec' and len(number) == 14: 166 # convert to decimal 167 number = '%010d%08d' % (int(number[0:8], 16), int(number[8:14], 16)) 168 if cd: 169 cd = calc_check_digit(number) 170 elif format == 'hex' and len(number) == 18: 171 # convert to hex 172 number = '%08X%06X' % (int(number[0:10]), int(number[10:18])) 173 if cd: 174 cd = calc_check_digit(number) 175 # see if we need to add a check digit 176 if add_check_digit and not cd: 177 cd = calc_check_digit(number) 178 # split number according to format 179 if len(number) == 14: 180 number = [number[i * 2:i * 2 + 2] 181 for i in range(7)] + [cd] 182 else: 183 number = (number[:5], number[5:10], number[10:14], number[14:], cd) 184 return separator.join(x for x in number if x) 185 186 187def to_binary(number): 188 """Convert the number to its binary representation (without the check 189 digit).""" 190 from binascii import a2b_hex 191 return a2b_hex(compact(number, strip_check_digit=True)) 192 193 194def to_pseudo_esn(number): 195 """Convert the provided MEID to a pseudo ESN (pESN). The ESN is returned 196 in compact hexadecimal representation.""" 197 import hashlib 198 # return the last 6 digits of the SHA1 hash prefixed with the reserved 199 # manufacturer code 200 return '80' + hashlib.sha1(to_binary(number)).hexdigest()[-6:].upper() 201