Last active
December 13, 2021 11:55
-
-
Save ptdecker/e907e0524db64fd988f093905709d3c9 to your computer and use it in GitHub Desktop.
Evaluates a mobile device serial number, determines its type (ESN, IMEI, MEID) and attempts to check for validity if possible.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// serialNumInfo() | |
// | |
// Evaluates a mobile device serial number, determines its type (ESN, IMEI, MEID) and attempts | |
// to check for validity if possible. | |
// | |
// When the passed purported serial number, the serialNumInfo() function returns a boolean flag | |
// ('isValid') indicating the validity of the number along with 'numType' indicating the type of | |
// serial number passed. The function expects the passed serialNumber to be not contain any | |
// spaces and for hexadecimal values to be passed without a leading "0x" or "0h" prefix. | |
// | |
// serialNumInfo() expects that the serial number is passed as a string and will log a console | |
// error and return 'isValid' of false if this is not the case. | |
// | |
// References: | |
// https://en.wikipedia.org/wiki/Electronic_serial_number | |
// http://cdg.org/news/events/CDMASeminar/05_LatinAm/op_roam_meeting/5_0306-CDG_IR-MEID(Qualcomm).pdf | |
// https://en.wikipedia.org/wiki/International_Mobile_Equipment_Identity | |
// https://www.gsma.com/newsroom/wp-content/uploads/2012/06/ts0660tacallocationprocessapproved.pdf | |
// https://en.wikipedia.org/wiki/Luhn_algorithm | |
// https://en.wikipedia.org/wiki/Luhn_mod_N_algorithm | |
function serialNumInfo(serialNum) { | |
// Regex expression definitions | |
const regexESNHex = new RegExp("^[0-9a-fA-F]{8}$"); // Hex ESN (eight digit mixed case hex) | |
const regexESNDec = new RegExp("^[0-9]{11}$"); // Decimal ESN candidate (eleven decimal digits further validation is possible) | |
const regexIMEINoCheckDec = new RegExp("^[0-9]{14}$"); // Decimal IMEI with no check digit (fourteen digit decimal) | |
const regexMEIDNoCheckHex = new RegExp("^[0-9a-fA-F]{14}$"); // Hex MEID with no check digit (fourteen digit mixed case hex) | |
const regexIMEIWithCheckDec = new RegExp("^[0-9]{15}$"); // Decimal IMEI with check digit (fifteen digit decimal) | |
const regexMEIDWithCheckHex = new RegExp("^[0-9a-fA-F]{15}$"); // Hex MEID with with check digit (fifteen digit mixed case hex) | |
const regexMEIDDec = new RegExp("^[0-9]{18}$"); // Decimal MEID (eighteen digit decimal) | |
var numInfo = { | |
isValid: false, | |
}; | |
// Exit if serial number is not a string | |
if (typeof serialNum !== "string") { | |
console.warn("'serialNumInfo()' passed an unexpected type (" + (typeof serialNum) + ") instead of a string"); | |
numInfo.errMessage = "Internal error: 'serialNumInfo()' passed an unexpected type"; | |
return numInfo; | |
}; | |
// Test for a hexadecimal ESN | |
if (regexESNHex.test(serialNum)) { | |
numInfo.isValid = true; | |
numInfo.numType = "ESN" | |
return numInfo; | |
}; | |
// Test for a decimal ESN | |
if (regexESNDec.test(serialNum)) { | |
if (parseInt(serialNum.substr(0, 3), 10) > 255) { // first three digits are the manufacturer code | |
numInfo.errMessage = "Appears to be a decimal formated ESN but the manufacturer code exceeds 255"; | |
return numInfo; | |
}; | |
if (parseInt(serialNum.substr(3, 8), 10) > 16777215) { // last eight digits is the serial number | |
numInfo.errMessage = "Appears to be a decimal formated ESN but the unique ID exceeds 16777215"; | |
return numInfo; | |
}; | |
numInfo.isValid = true; | |
numInfo.numType = "ESN" | |
return numInfo; | |
}; | |
// Test for fourteen digit hexadecimal MEID (i.e. an MEID with no check digit) | |
if (regexMEIDNoCheckHex.test(serialNum) && !regexIMEINoCheckDec.test(serialNum)) { | |
if (hexToDec(serialNum, 0, 2) < 160) { // first two digits are the region code | |
numInfo.errMessage = "Appears to be a hexadecimal MEID but region code is less than 160 (0xA0)"; | |
return numInfo; | |
}; | |
numInfo.isValid = true; | |
numInfo.numType = "MEID" | |
return numInfo; | |
}; | |
// Test for fourteen digit decimal IMEI (i.e. an IMEI with no check digit) | |
if (regexIMEINoCheckDec.test(serialNum)) { | |
numInfo.isValid = true; | |
numInfo.numType = "IMEI" | |
return numInfo; | |
}; | |
// Test for fifteen digit decimal IMEI | |
if (regexIMEIWithCheckDec.test(serialNum)) { | |
if (calcLuhnModN(serialNum.substr( 0, 14), false, 10) !== serialNum.substr(14, 1)) { | |
numInfo.errMessage = "Appears to be an IMEI however the calculated check digit is incorrect"; | |
return numInfo; | |
} | |
numInfo.isValid = true; | |
numInfo.numType = "IMEI" | |
return numInfo; | |
}; | |
// Test for fifteen digit hex MEID | |
if (regexMEIDWithCheckHex.test(serialNum)) { | |
if (hexToDec(serialNum, 0, 2) < 160) { | |
numInfo.errMessage = "Appears to be a hexadecimal MEID but region code is less than 160 (0xA0)"; | |
return numInfo; | |
}; | |
if (calcLuhnModN(serialNum.substr(0, 14), false, 16) !== serialNum.substr(14, 1)) { | |
numInfo.errMessage = "Appears to be an MEID however the calculated check digit is incorrect"; | |
return numInfo; | |
} | |
numInfo.isValid = true; | |
numInfo.numType = "MEID" | |
return numInfo; | |
}; | |
// Test for eighteen digit decimal MEID | |
if (regexMEIDDec.test(serialNum)) { | |
const mfgCode = padWithZeros(parseInt(serialNum.substr( 0, 10), 10).toString(16), 8); | |
const uniqueId = padWithZeros(parseInt(serialNum.substr(10, 8), 10).toString(16), 6); | |
const newSerialNum = mfgCode + uniqueId; | |
if (regexMEIDNoCheckHex.test(newSerialNum) && !regexIMEINoCheckDec.test(newSerialNum) && hexToDec(newSerialNum, 0, 2) < 160) { | |
numInfo.errMessage = "Appears to be an MEID but region code is less than 160"; | |
return numInfo; | |
}; | |
numInfo.isValid = true; | |
numInfo.numType = "MEID" | |
return numInfo; | |
}; | |
// Return serial number info | |
numInfo.errMessage = "Invalid device serial number" | |
return numInfo; | |
}; // serialNumInfo() | |
// calcLuhnModN() | |
// | |
// Helper function that calculates and returns the Luhn digit from a serial number 'serialNum'. | |
// 'includesCheckDigit' is Boolean true if the serial number passed includes the check digit | |
// at the end. A value of false indicates the check digit is not included. It can handle both | |
// decimal and hexadecimal values by passing a radix value of 10 or 16 respectively. A radix | |
// value of anything between 1 and 16 would be supported but only 10 and 16 have been verified. | |
// | |
// | |
function calcLuhnModN(serialNum, includesCheckDigit, radix) { | |
var sum = 0; | |
var parity = serialNum.length % 2; | |
for (var i = 0; i < (serialNum.length - (includesCheckDigit ? 1 : 0)); i++) { | |
let codePoint = parseInt(serialNum[i], radix); | |
let doubleDigit = ((((i + (includesCheckDigit ? 0 : 1)) % 2) === parity) ? 2 : 1) * codePoint; | |
let sumOfDigits = (Math.floor(doubleDigit / radix) + doubleDigit % radix); | |
sum += sumOfDigits; | |
} | |
return ((radix - (sum % radix)) % radix).toString(radix).toUpperCase(); | |
}; // calcLuhnModN() | |
// hexToDec() | |
// | |
// Helper function that converts the characters of a string 's' starting at 'n' of length 'len' from hex to decimal | |
function hexToDec(s, pos, len) { | |
return parseInt(s.substr(pos, len), 16).toString(10); | |
}; // hexToDec() | |
// padWithZeros() | |
// | |
// Helper function that pads a string 'n' to a length of 'len' with leading zeros | |
function padWithZeros(n, len) { | |
n = n + ''; // convert null and undefined values to an empty string | |
return (n.length > len) ? n : new Array(len - n.length + 1).join("0") + n; | |
}; // padWithZeros() | |
function testValidity(serialNum, expectedResult) { | |
return (serialNumInfo(serialNum).isValid === expectedResult); | |
}; // testValidity() | |
function runTests() { | |
// Sources: | |
// [1] VMU QA team test data | |
// [2] Developer created test data | |
const tests = [ | |
{serialNum:12313, isValid:false}, // Unexpected type [2] | |
{serialNum:"0106C01B", isValid:true}, // Hex ESN [2] | |
{serialNum:"e5ab0134", isValid:true}, // Hex ESN [2] | |
{serialNum:"80ab0134", isValid:true}, // Hex pESN [2] | |
{serialNum:"e5ab0qb4", isValid:false}, // Invalid hex ESN [2] | |
{serialNum:"80abre34", isValid:false}, // Invalid hex pESN [2] | |
{serialNum:"01212424532", isValid:true}, // Decimal ESN [2] | |
{serialNum:"12812424532", isValid:true}, // Decimal pESN [2] | |
{serialNum:"29012424532", isValid:false}, // Invalid decimal ESN [2] | |
{serialNum:"01220072888", isValid:false}, // Invalid decimal ESN [2] | |
{serialNum:"12820072888", isValid:false}, // Invalid decimal pESN [2] | |
{serialNum:"A0B456789FC234", isValid:true}, // Hex MEID w/o checksum [2] | |
{serialNum:"34B456789FC234", isValid:false}, // Invalid Hex MEID w/o check digit [2] | |
{serialNum:"089872794300010609", isValid:true}, // 18-digit decimal MEID [1] | |
{serialNum:"089872461500164869", isValid:true}, // 18-digit decimal MEID [1] | |
{serialNum:"089872461500103297", isValid:true}, // 18-digit decimal MEID [1] | |
{serialNum:"089170074301603352", isValid:true}, // 18-digit decimal MEID [1] | |
{serialNum:"089170074301644648", isValid:true}, // 18-digit decimal MEID [1] | |
{serialNum:"089170074301646901", isValid:true}, // 18-digit decimal MEID [1] | |
{serialNum:"35918007002971", isValid:true}, // 14-digit IMEI [1] | |
{serialNum:"35917307028405", isValid:true}, // 14-digit IMEI [1] | |
{serialNum:"35917307019381", isValid:true}, // 14-digit IMEI [1] | |
{serialNum:"35264607187718", isValid:true}, // 14-digit IMEI [1] | |
{serialNum:"35264607191868", isValid:true}, // 14-digit IMEI [1] | |
{serialNum:"35264607192135", isValid:true}, // 14-digit IMEI [1] | |
{serialNum:"A0000000100001", isValid:true}, // 14-digit MEID [2] | |
{serialNum:"A0000000100002", isValid:true}, // 14-digit MEID [2] | |
{serialNum:"A0000000100003", isValid:true}, // 14-digit MEID [2] | |
{serialNum:"A000000010004B", isValid:true}, // 14-digit MEID [2] | |
{serialNum:"3200000010004B", isValid:false}, // Invalid 14-digit MEID [2] | |
{serialNum:"359180070029712", isValid:true}, // 15-digit IMEI [1] | |
{serialNum:"359173070284056", isValid:true}, // 15-digit IMEI [1] | |
{serialNum:"359173070193810", isValid:true}, // 15-digit IMEI [1] | |
{serialNum:"352646071877181", isValid:true}, // 15-digit IMEI [1] | |
{serialNum:"352646071918688", isValid:true}, // 15-digit IMEI [1] | |
{serialNum:"352646071921351", isValid:true}, // 15-digit IMEI [1] | |
{serialNum:"359180070029714", isValid:false}, // Invalid 15-digit IMEI (check digit bad) [2] | |
{serialNum:"359173091284056", isValid:false}, // Invalid 15-digit IMEI (internal digit bad) [2] | |
{serialNum:"A00000001000013", isValid:true}, // 15-digit MEID [2] | |
{serialNum:"A00000001000021", isValid:true}, // 15-digit MEID [2] | |
{serialNum:"A0000000100003F", isValid:true}, // 15-digit MEID [2] | |
{serialNum:"A000000010004BA", isValid:true} // 15-digit MEID [2] | |
]; | |
var globalPass = true; | |
console.log("Validity checks:") | |
for (var i = 0; i < tests.length; i++) { | |
let testNum = (1000+i).toString().substring(1); | |
if (testValidity(tests[i].serialNum, tests[i].isValid)) { | |
console.log("\t" + testNum + ': "' + tests[i].serialNum + '": Pass'); | |
console.log("\t\t" + JSON.stringify(serialNumInfo(tests[i].serialNum))); | |
} else { | |
console.log("\t" + testNum + ': "' + tests[i].serialNum + '": Fail'); | |
globalPass = false; | |
}; | |
}; | |
if (globalPass) { | |
console.log("All test cases passed"); | |
} else { | |
console.error("One or more tests failed"); | |
}; | |
} // runTests() | |
runTests(); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment