Files
psa_car_controller/otp/otp.py
T

345 lines
12 KiB
Python

import hashlib
import pickle
from secrets import token_hex, token_bytes
from math import ceil
from collections import defaultdict
from xml.etree import cElementTree as ElT
import requests
from Cryptodome.Cipher import AES
from Cryptodome.PublicKey import RSA
from Cryptodome import Hash
from mylogger import logger
from . import oaep
from .load import IWData
# pylint: disable=too-many-instance-attributes,invalid-name
CONFIG_NAME = "otp.bin"
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 number_to_base36(n):
b = 36
if n == 0:
return [0]
digits = ""
while n:
digits += base36[int(n % b)]
n //= b
return digits
class ConfigException(Exception):
"""Raise exception if otp config isn't correct"""
class Otp:
OTP_TWICE = 10
OK = 0
NOK = -1
KPub = "11"
exponent = int(KPub, 16)
ACTIVATE_MODE = "activate"
OTP_MODE = "otp"
MS_MODE = "ms"
iw_host = "https://otp.mpsa.com"
proxies = None
def __init__(self, inwebo_access_id, device_id=token_hex(8)):
self.Kiw = None
self.pinmode = None
self.Kfact = None
self.pinmode = None
self.needsync = None
self.serviceid = None
self.alias = None
self.iwalea = token_hex(16)
self.device_id = device_id
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 = inwebo_access_id
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_oaep(Kiw, self.Kfact)
key = RSA.construct((int(self.Kiw, 16), Otp.exponent))
self.cipher = oaep.new(key, hash_algo=Hash.SHA256)
def get_serial(self):
return self.device_id + "/_/" + self.iwalea
def generate_kma(self, codepin):
serial = self.get_serial()
kma_str = codepin + ";" + serial
kma = hashlib.sha256(kma_str.encode("utf-8")).hexdigest()[:32]
return kma
def get_r(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.get_serial()
R1 = self.challenge + ";" + iw + ";" + self.data.iwK1
logger.debug("%s\n%s\n%s", R0, R1, 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()}
@staticmethod
def decode_oaep(enc, key):
modulus = int(key, 16)
key = RSA.construct((modulus, Otp.exponent))
cipher = oaep.new(key, hash_algo=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:
maxi = len(enc_b)
else:
maxi = (1 + x) * 128
mini = x * 128
ciphertext = cipher.decrypt(enc_b[mini:maxi])
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=self.proxies,
verify=self.proxies is None
).text
try:
raw_xml = raw_xml[raw_xml.index("?>") + 2:]
if setup:
return etree_to_dict(ElT.XML(raw_xml))["ActionSetup"]
return etree_to_dict(ElT.XML(raw_xml))["ActionFinalize"]
except KeyError:
logger.debug(raw_xml)
raise ValueError("Bad response from server") from KeyError
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})
elif 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
return False
def activation_finalyze(self, random_bytes=None):
R = self.get_r()
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.generate_kma(self.codepin))).hex()
pin_crypt = self.cipher.encrypt(self.codepin.encode("utf-8")).hex()
params.update({"serial": self.get_serial(), "code": self.smsCode,
"Kma": kma_crypt, "pin": pin_crypt, "name": "Android SDK built for x86_64 / UNKNOWN", })
params.update(R)
xml = self.request(params)
if xml["err"] != "OK":
logger.error("Error during activation: %s", xml)
return Otp.NOK
self.data.synchro(xml, self.generate_kma(self.codepin))
if self.mode == Otp.OTP_MODE:
try:
self.defi = str(xml["defi"])
except KeyError:
raise ConfigException from KeyError
if "J" in xml:
logger.debug("Need another otp request")
return Otp.OTP_TWICE
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
ms_n = "0"
self.challenge = xml["challenge"]
self.action = "synchro"
res = self.decode_oaep(xml["ms_key"], self.Kfact)
temp_key = RSA.construct((int(res, 16), self.exponent))
temp_cipher = oaep.new(temp_key, hash_algo=Hash.SHA256)
if random_bytes is None:
random_bytes = token_bytes(16)
kpub_encode = temp_cipher.encrypt(random_bytes)
aes_cipher = AES.new(bytes.fromhex(self.generate_kma(self.codepin)), AES.MODE_ECB)
encode_aes_from_hex = aes_cipher.encrypt(random_bytes).hex()
self.data.iwsecval = encode_aes_from_hex
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: kpub_encode.hex(), "macid": self.macid}
req_param.update({"id": self.data.iwid, "lastsync": self.data.iwTsync, "ms_n": 1})
req_param.update(self.get_r())
xml = self.request(req_param)
self.data.synchro(xml, self.generate_kma(self.codepin))
return Otp.OK
def _get_otp_code(self):
password = self.data.iwK1 + ":" + str(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 = number_to_base36(nb)
return otp
def get_otp_code(self):
self.mode = Otp.OTP_MODE
otp_code = None
if self.activation_start():
res = self.activation_finalyze()
if res != Otp.NOK:
if res == Otp.OTP_TWICE:
self.mode = Otp.OTP_MODE
self.activation_start()
self.activation_finalyze()
otp_code = self._get_otp_code()
logger.debug("otp code: %s", otp_code)
if otp_code is None:
raise ConfigException("Can't get otp code")
return otp_code
def __getstate__(self):
odict = self.__dict__.copy()
if 'cipher' in odict:
del odict['cipher'] # don't pickle this
return odict
def __setstate__(self, dict_param):
self.__dict__.update(dict_param)
if self.Kiw is not None:
key = RSA.construct((int(self.Kiw, 16), Otp.exponent))
self.cipher = oaep.new(key, hash_algo=Hash.SHA256)
@staticmethod
def set_proxies(proxies):
Otp.proxies = proxies
def encode_oeap(text, key):
cipher = oaep.new(bytes.fromhex(key), hash_algo=Hash.SHA256)
return cipher.encrypt(text)
def save_otp(obj, filename="otp.bin"):
with open(filename, 'wb') as output:
pickle.dump(obj, output)
class RenameUnpickler(pickle.Unpickler):
def find_class(self, module, name):
renamed_module = module
if module == 'otp.Otp':
renamed_module = "otp.otp"
elif module == 'otp.Tokenizer':
renamed_module = "otp.tokenizer"
return super().find_class(renamed_module, name)
def load_otp(filename=CONFIG_NAME):
try:
with open(filename, 'rb') as input_file:
try:
return pickle.load(input_file)
except ModuleNotFoundError:
return RenameUnpickler(input_file).load()
except FileNotFoundError:
logger.debug("", exc_info=True)
return None
def new_otp_session(old_otp_session: Otp = None, smscode=None, codepin=None):
if old_otp_session is None:
otp = Otp("bb8e981582b0f31353108fb020bead1c")
else:
otp = Otp("bb8e981582b0f31353108fb020bead1c", device_id=old_otp_session.device_id)
if smscode is None:
otp.smsCode = input("What is the code you just received by SMS ?")
otp.codepin = input("What is your app pin code ?")
else:
otp.smsCode = smscode
otp.codepin = codepin
otp.activation_start()
otp.activation_finalyze()
save_otp(otp)
return otp