mirror of
https://github.com/flobz/psa_car_controller.git
synced 2026-08-22 09:26:16 +00:00
304 lines
10 KiB
Python
304 lines
10 KiB
Python
import hashlib
|
|
import traceback
|
|
from secrets import token_hex, token_bytes
|
|
|
|
import requests
|
|
from Cryptodome.Cipher import AES
|
|
from Cryptodome.PublicKey import RSA
|
|
from Cryptodome import Hash
|
|
from math import ceil
|
|
|
|
|
|
|
|
from collections import defaultdict
|
|
from xml.etree import cElementTree as ET
|
|
|
|
from otp import oaep
|
|
from otp.load import IWData
|
|
import pickle
|
|
from MyLogger import logger
|
|
proxies = None
|
|
|
|
|
|
def etree_to_dict(t):
|
|
d = {t.tag: {} if t.attrib else None}
|
|
children = list(t)
|
|
if children:
|
|
dd = defaultdict(list)
|
|
for dc in map(etree_to_dict, children):
|
|
for k, v in dc.items():
|
|
dd[k].append(v)
|
|
d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}}
|
|
if t.attrib:
|
|
d[t.tag].update(('@' + k, v) for k, v in t.attrib.items())
|
|
if t.text:
|
|
text = t.text.strip()
|
|
if children or t.attrib:
|
|
if text:
|
|
d[t.tag]['#text'] = text
|
|
else:
|
|
d[t.tag] = text
|
|
return d
|
|
|
|
|
|
base36 = ["a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v",
|
|
"w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9"]
|
|
|
|
|
|
def numberToBase36(n):
|
|
b = 36
|
|
if n == 0:
|
|
return [0]
|
|
digits = ""
|
|
while n:
|
|
digits += base36[int(n % b)]
|
|
n //= b
|
|
return digits
|
|
|
|
|
|
class Otp:
|
|
OTP_TWICE = 10
|
|
OK=0
|
|
kPub = "11"
|
|
exponent = int(kPub, 16)
|
|
ACTIVATE_MODE = "activate"
|
|
OTP_MODE = "otp"
|
|
MS_MODE = "ms"
|
|
iw_host = "https://otp.mpsa.com"
|
|
def __init__(self, inweboAccessId):
|
|
self.Kiw = None
|
|
self.pinmode = None
|
|
self.Kfact = None
|
|
self.Kiw = None
|
|
self.pinmode = None
|
|
self.needsync = None
|
|
self.serviceid = None
|
|
self.alias = None
|
|
self.iwalea = token_hex(16)
|
|
self.device_id = token_hex(8)
|
|
self.codepin = None
|
|
self.challenge = ""
|
|
self.action = ""
|
|
self.s_id = None
|
|
self.version = "0.2.11"
|
|
self.isMac = True
|
|
self.data = IWData(self)
|
|
self.cipher = None
|
|
self.macid = inweboAccessId
|
|
self.smsCode = None
|
|
self.mode = Otp.ACTIVATE_MODE
|
|
self.defi = 0
|
|
self.otp_count = 0
|
|
|
|
def init(self, Kfact=None, Kiw=None, pinmode=None):
|
|
self.Kfact = Kfact
|
|
self.pinmode = pinmode
|
|
self.Kiw = self.decode_oeap(Kiw, self.Kfact)
|
|
key = RSA.construct((int(self.Kiw, 16), Otp.exponent))
|
|
self.cipher = oaep.new(key, hashAlgo=Hash.SHA256)
|
|
|
|
def getSerial(self):
|
|
return self.device_id + "/_/" + self.iwalea
|
|
|
|
def generateKMA(self, codepin):
|
|
serial = self.getSerial()
|
|
kma_str = codepin + ";" + serial
|
|
kma = hashlib.sha256(kma_str.encode("utf-8")).hexdigest()[:32]
|
|
return kma
|
|
|
|
def getR(self):
|
|
if self.action == "upgrade":
|
|
iw = self.data.iwK1
|
|
# not correctly implemented
|
|
else:
|
|
iw = self.data.iwK0
|
|
if self.action == "synchro":
|
|
R2 = self.challenge + ";" + iw + ";" + self.codepin
|
|
else:
|
|
R2 = self.challenge + ";" + iw + ";"
|
|
|
|
R0 = self.challenge + ";" + iw + ";" + self.getSerial()
|
|
R1 = self.challenge + ";" + iw + ";" + self.data.iwK1
|
|
logger.debug(f"{R0}\n{R1}\n{R2}")
|
|
return {"R0": hashlib.sha256(R0.encode("utf-8")).hexdigest(),
|
|
"R1": hashlib.sha256(R1.encode("utf-8")).hexdigest(),
|
|
"R2": hashlib.sha256(R2.encode("utf-8")).hexdigest()}
|
|
|
|
def decode_oeap(self, enc, key):
|
|
modulus = int(key, 16)
|
|
key = RSA.construct((modulus, Otp.exponent))
|
|
cipher = oaep.new(key, hashAlgo=Hash.SHA256)
|
|
block_size = 128
|
|
dec_string = ""
|
|
enc_b = bytes.fromhex(enc)
|
|
nb_block = ceil(len(enc_b) / block_size)
|
|
|
|
for x in range(0, nb_block):
|
|
if x == nb_block - 1:
|
|
max = len(enc_b)
|
|
else:
|
|
max = (1 + x) * 128
|
|
min = x * 128
|
|
ciphertext = cipher.decrypt(enc_b[min:max])
|
|
dec_string += ciphertext.hex()
|
|
logger.debug(dec_string)
|
|
return dec_string
|
|
|
|
def request(self, param, setup=False):
|
|
raw_xml = requests.get(
|
|
f"{self.iw_host}/iwws/MAC",
|
|
headers={
|
|
"Connection": "Keep-Alive",
|
|
"Host": "otp.mpsa.com",
|
|
"User-Agent": "Dalvik/2.1.0 (Linux; U; Android 8.0.0; Android SDK built for x86_64 Build/OSR1.180418.004)"
|
|
},
|
|
params=param,
|
|
proxies=proxies,
|
|
verify=False
|
|
).text
|
|
try:
|
|
raw_xml = raw_xml[raw_xml.index("?>") + 2:]
|
|
if setup:
|
|
return etree_to_dict(ET.XML(raw_xml))["ActionSetup"]
|
|
else:
|
|
return etree_to_dict(ET.XML(raw_xml))["ActionFinalize"]
|
|
|
|
except:
|
|
logger.debug(raw_xml)
|
|
raise Exception("Bad return from server")
|
|
|
|
def activation_start(self):
|
|
param = {"action": "ActionSetup", "mode": self.mode, "id": self.data.iwid, "lastsync": self.data.iwTsync,
|
|
"version": "Generator-1.0/0.2.11", "macid": self.macid}
|
|
if self.mode == Otp.OTP_MODE:
|
|
param.update({"sid": self.data.iwsecid})
|
|
if self.mode == Otp.ACTIVATE_MODE:
|
|
param.update({"code": self.smsCode})
|
|
|
|
xml = self.request(param, setup=True)
|
|
if xml["err"] == "OK":
|
|
if self.mode == Otp.ACTIVATE_MODE:
|
|
xml_filtred = {key: xml[key] for key in ["Kiw", "Kfact", "pinmode"]}
|
|
self.init(**xml_filtred)
|
|
elif self.mode == Otp.OTP_MODE:
|
|
self.challenge = xml["challenge"]
|
|
return True
|
|
else:
|
|
return False
|
|
|
|
def activation_finalyze(self, random_bytes=None):
|
|
|
|
R = self.getR()
|
|
params = {"action": "ActionFinalize", "mode": self.mode, "id": self.data.iwid, "lastsync": self.data.iwTsync,
|
|
"version": "Generator-1.0/0.2.11",
|
|
"lang": "fr", "ack": "", "macid": self.macid}
|
|
if self.mode == Otp.OTP_MODE:
|
|
params.update({"keytype": '0', "sid": self.data.iwsecid})
|
|
|
|
elif self.mode == Otp.ACTIVATE_MODE:
|
|
kma_crypt = self.cipher.encrypt(bytes.fromhex(self.generateKMA(self.codepin))).hex()
|
|
pin_crypt = self.cipher.encrypt(self.codepin.encode("utf-8")).hex()
|
|
params.update({"serial": self.getSerial(), "code": self.smsCode,
|
|
"Kma": kma_crypt, "pin": pin_crypt, "name": "Android SDK built for x86_64 / UNKNOWN", })
|
|
|
|
params.update(R)
|
|
xml = self.request(params)
|
|
|
|
self.data.synchro(xml, self.generateKMA(self.codepin))
|
|
|
|
if self.mode == Otp.OTP_MODE:
|
|
self.defi = str(xml["defi"])
|
|
if "J" in xml:
|
|
logger.debug("Need another otp request")
|
|
return Otp.OTP_TWICE
|
|
else:
|
|
return Otp.OK
|
|
|
|
if "ms_n" not in xml or xml["ms_n"] == 0:
|
|
logger.debug("no ms_n request needed")
|
|
return Otp.OK
|
|
|
|
if int(xml["ms_n"]) > 1:
|
|
raise NotImplementedError
|
|
else:
|
|
ms_n = "0"
|
|
|
|
self.challenge = xml["challenge"]
|
|
self.action = "synchro"
|
|
res = self.decode_oeap(xml["ms_key"], self.Kfact)
|
|
temp_key = RSA.construct((int(res, 16), self.exponent))
|
|
temp_cipher = oaep.new(temp_key, hashAlgo=Hash.SHA256)
|
|
if random_bytes is None:
|
|
random_bytes = token_bytes(16)
|
|
KpubEncode = temp_cipher.encrypt(random_bytes)
|
|
|
|
aes_cipher = AES.new(bytes.fromhex(self.generateKMA(self.codepin)), AES.MODE_ECB)
|
|
encodeAesFromHex = aes_cipher.encrypt(random_bytes).hex()
|
|
self.data.iwsecval = encodeAesFromHex
|
|
self.data.iwsecid = xml["s_id"]
|
|
self.data.iwsecn = 1
|
|
|
|
req_param = {"action": "ActionFinalize", "mode": Otp.MS_MODE, "ms_id" + ms_n: xml["ms_id"],
|
|
"ms_val" + ms_n: KpubEncode.hex(), "macid": self.macid}
|
|
req_param.update({"id": self.data.iwid, "lastsync": self.data.iwTsync, "ms_n": 1})
|
|
req_param.update(self.getR())
|
|
xml = self.request(req_param)
|
|
self.data.synchro(xml, self.generateKMA(self.codepin))
|
|
return Otp.OK
|
|
|
|
def _getOtpCode(self):
|
|
password = self.data.iwK1 + ":" + self.defi + ":" + self.data.iwsecval
|
|
res = bytes(hashlib.sha256(password.encode("utf-8")).digest())
|
|
nb = ((int.from_bytes(res[:4], byteorder="big") & 0xfffffff) * 1024) + (
|
|
int.from_bytes(res[4:8], byteorder="big") & 1023)
|
|
otp = numberToBase36(nb)
|
|
return otp
|
|
|
|
def getOtpCode(self):
|
|
self.mode = Otp.OTP_MODE
|
|
self.activation_start()
|
|
res = self.activation_finalyze()
|
|
if res == Otp.OTP_TWICE:
|
|
self.mode = Otp.OTP_MODE
|
|
self.activation_start()
|
|
self.activation_finalyze()
|
|
otp_code = self._getOtpCode()
|
|
logger.debug(f"otp code: {otp_code}")
|
|
return otp_code
|
|
|
|
def __getstate__(self):
|
|
odict = self.__dict__.copy()
|
|
del odict['cipher'] # don't pickle this
|
|
return odict
|
|
|
|
def __setstate__(self, dict):
|
|
self.__dict__.update(dict)
|
|
if self.Kiw is not None:
|
|
key = RSA.construct((int(self.Kiw, 16), Otp.exponent))
|
|
self.cipher = oaep.new(key, hashAlgo=Hash.SHA256)
|
|
|
|
|
|
def encode_oeap(text, key):
|
|
cipher = oaep.new(bytes.fromhex(key), hashAlgo=Hash.SHA256)
|
|
return cipher.encrypt(text)
|
|
|
|
def save_otp(obj):
|
|
with open("otp.bin", 'wb') as output:
|
|
pickle.dump(obj, output)
|
|
|
|
def load_otp():
|
|
try:
|
|
with open("otp.bin", 'rb') as input:
|
|
return pickle.load(input)
|
|
except:
|
|
logger.debug(traceback.format_exc())
|
|
return None
|
|
|
|
def new_otp_session():
|
|
otp = Otp("bb8e981582b0f31353108fb020bead1c")
|
|
otp.smsCode = input("What is the code you just received by SMS ?")
|
|
otp.codepin = input("What is your app pin code ?")
|
|
otp.activation_start()
|
|
otp.activation_finalyze()
|
|
save_otp(otp)
|
|
return otp |