https://github.com/mcphail/linux_huawei_unlocker/blob/master/unlocker.py

Python 3 mods/fixes

#!/usr/bin/env python
#
# unlocker.py
# - Remove simlock on huawei modems
#
# Copyright (C) 2013 Neil McPhail
#        [email protected]
#
# Unlock code generator Copyright (C) 2010 dogbert
#                                     [email protected]

# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

import time
import re
import hashlib
import glob
import sys
import serial

# Intro
def intro():
        print(80 * "*")
        print("\tHuawei modem unlocker")
        print("\tBy Neil McPhail and dogbert")
        print("\tThis is Free Software as defined by the GNU GENERAL PUBLIC")
        print("\tLICENSE version 2")
        print(80 * "*")
        print("\tThis software comes with NO WARRANTY")
        print("\tThis software can damage your hardware")
        print("\tUse it at your own risk")
        print(80 * "*")
        print("\tNot all modems can be unlocked with this software.")
        print("\tUsers have reported problems with the following devices:")
        print("\t\tE220, E353")
        print("\tAttempting to unlock these devices with this software is not")
        print("\trecommended. I hope to fix this in a later release.")
        print(80 * "*")
        if not require_yes():
                print("Bye")
                sys.exit(0)

# Helper function
# Require an explicit "YES" in upper case
# Returns True if "YES"
# Asks for explicit uppercase "YES" if mixed or lower case used
# Returns False for anything else
def require_yes():
    print("If you wish to proceed, please type YES at the prompt")
    while 1:
        response = input(">> ")
        if response == "YES":
            return True
        if response.upper() == "YES":
            print("You must type YES in upper case to proceed")
            continue
        else:
            return False

# These modems seem to open 3 USB serial ports. Only one is the control port
# and this seems to vary from device to device. The other 2 ports appear to
# remain silent
def identifyport():
    print("Trying to find which port is the active modem connection.")
    print("Please be patient as this can take a while.\n\n")
    for p in glob.glob('/dev/ttyUSB*'):
        print("Testing port " + p)
        ser = serial.Serial(port = p, timeout = 15, rtscts = True, dsrdtr = True)
        ser.write('AT\r\n')
        activity = ser.read(5)
        if activity == '':
            print("\tNo activity\n")
            ser.close()
            continue

        print("\tActivity detected\n")
        ser.close()
        return p
    return ''


# The modem should respond with the IMEI with the AT+CGSN command
def obtainimei(port):
    print("\nTrying to obtain IMEI.")
    print("The modem will be given 5 seconds to respond.")
    ser = serial.Serial(port = port, timeout = 0, rtscts = True, dsrdtr = True)
    ser.reset_input_buffer()
    ser.write('AT+CGSN\r\n')
    time.sleep(5)
    response = ser.read(4096)
    ser.close()
    match = re.search(r'\r\n(\d{15})\r\n', response)
    if match:
        print("Found probable IMEI: " + match.group(1))
        return match.group(1)
    else:
        print("IMEI not found")
        return ''

# Check the IMEI is correct
# Adapted from dogbert's original
def testimeichecksum(imei):
    digits = []
    for i in imei:
        digits.append(int(i))
    _sum = 0
    alt = False
    for d in reversed(digits):
        assert 0 <= d <= 9
        if alt:
            d *= 2
        if d > 9:
            d -= 9
        _sum += d
        alt = not alt
    return (_sum % 10) == 0

# Display a warning if first digit of IMEI indicates a potentially troublesome
# modem
def checkimeicompatibility(imei):
    if '8' == imei[0]:
        print(80 * "*")
        print("\n\tWarning")
        print("\n\tYour modem's IMEI begins with '8'")
        print("\tIt is likely to be incompatible with this script")
        print("\tProceed at your own risk")
        print("\n\tPlease provide feedback: see README and HELPME\n")
        print(80 * "*")

# Interrogate the lock status
# Returns a dictionary with the lock status, remaining unlock attempts
# and the - largely unused - carrier code
#
# lockStatus 0 = unobtainable
#            1 = locked but can be unlocked
#            2 = unlocked to the inserted sim
#            3 = locked and cannot be unlocked
def checklockstatus(port):
    status = {'lockStatus': 0, 'remaining': 0, 'carrier': 0}
    print("\nChecking the lock status of the SIM.")
    print("The modem will be given 5 seconds to respond.")
    ser = serial.Serial(port = port, timeout = 0, rtscts = True, dsrdtr = True)
    ser.reset_input_buffer()
    ser.write('AT^CARDLOCK?\r\n')
    time.sleep(5)
    response = ser.read(4096)
    ser.close()

    match = re.search(r'CARDLOCK: (\d),(\d\d?),(\d+)\r', response)
    if match:
        status['lockStatus'] = int(match.group(1))
        status['remaining'] = int(match.group(2))
        status['carrier'] = int(match.group(3))
    return status

# Compute the unlock code
# Adapted from dogbert's original
def computeunlockcode(imei):
    salt = '5e8dd316726b0335'
    digest = hashlib.md5((imei+salt).lower()).digest()
    code = 0
    for i in range(0,4):
        code += (ord(digest[i])^ord(digest[4+i])^ord(digest[8+i])^ord(digest[12+i])) << (3-i)*8
    code &= 0x1ffffff
    code |= 0x2000000
    return code

# Send AT codes to unlock the modem
def unlockmodem(port, lockcode):
    ser = serial.Serial(port = port, rtscts = True, dsrdtr = True)
    command = 'AT^CARDLOCK="'+ str(lockcode) + '"\r\n'
    ser.write(command)
    ser.close()
#
# Main routine
#
def main():
    intro()
    # Work out which is the control port
    try:
        activeport = identifyport()
    except:
        print("\nAn error occurred when probing for active ports.")
        print("This may be because you need to run this program as root.")
        sys.exit(1)
    else:
        if activeport=='':
            print("\nCould not identify active port.")
            sys.exit(1)

    # Obtain and check IMEI
    try:
        imei = obtainimei(activeport)
    except:
        print("\nAn error occurred when trying to check the IMEI.")
        sys.exit(1)
    else:
        if imei=='':
            print("\nCould not obtain IMEI.")
            print("Check the modem is properly inserted")
            print("Check a SIM card is in place")
            print("Check you are not already connected")
            print("Try removing and reinserting the device")
            sys.exit(1)
        else:
            if not testimeichecksum(imei):
                print("\nIMEI checksum invalid.")
                sys.exit(1)
            else:
                print("IMEI checksum OK.")
                checkimeicompatibility(imei)

    # Obtain lockstatus
    try:
        lockinfo = checklockstatus(activeport)
    except:
        print("\nAn error occurred when trying to check the SIM lock.")
        sys.exit(1)
    else:
        ls = lockinfo['lockStatus']
        if ls == 0:
            print("\nCouldn't obtain SIM lock status.")
            print("Further operations would be dangerous.")
            sys.exit(1)
        elif ls == 2:
            print("\nThe modem is already unlocked for this SIM.")
            sys.exit(0)
        elif ls == 3:
            print("\nThe modem is hard locked,")
            print("This program cannot help you.")
            sys.exit(1)
        else:
            print("\nThis SIM should be unlockable...")
            print("Remaining attempts: ", lockinfo['remaining'])
            print("Exceeding this will hard-lock the modem")

    unlockcode = computeunlockcode(imei)
    print("\nUnlock code = ", unlockcode)
    print("Please be aware that a failed unlocking attempt could break your modem.")
    print("This is a risky procedure.")
    if not require_yes():
        print("Unlocking aborted")
        sys.exit(0)

    print("\nAttempting to unlock...")
    try:
        unlockmodem(activeport, unlockcode)
    except:
        print("\nAn error occurred when trying to unlock the modem.")
        sys.exit(1)

    print("\nWill check result in 5 seconds.")
    time.sleep(5)

    # Check result
    try:
        lockinfo = checklockstatus(activeport)
    except:
        print("\nAn error occurred when trying to check the SIM lock.")
        sys.exit(1)
    else:
        ls = lockinfo['lockStatus']
        if ls == 0:
            print("\nCouldn't obtain SIM lock status.")
            print("Further operations would be dangerous.")
            sys.exit(1)
        elif ls == 1:
            print("\nUnlocking unsuccessful. Sorry.")
            sys.exit(1)
        elif ls == 3:
            print("\nUnlocking unsuccessful.")
            print("The modem appears to have been hard locked. Sorry.")
            sys.exit(1)
        else:
            print("\nUnlocking successful!")

if __name__ == "__main__":
    main()