Skip to content

Instantly share code, notes, and snippets.

@ptdecker
Last active December 13, 2021 11:55
Show Gist options
  • Save ptdecker/e907e0524db64fd988f093905709d3c9 to your computer and use it in GitHub Desktop.
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.
// 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