#!/usr/bin/env python3
# -*- coding: utf-8 -*-

"""
Gadgets to modify SYSMO USIM SJS1 parameters

(C) 2017 by Sysmocom s.f.m.c. GmbH
All Rights Reserved

Author: Philipp Maier

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, see <http://www.gnu.org/licenses/>.
"""

# Some gadgets to handle functions specific to Sysmo USIM SJS1. The gadgets are
# organized as a loose collection of python functions. Each function serves
# a specific task (e.g. modifiying the auth parameters). For each task two
# functions are implemented sysmo_usim_show_...() to inspect the data that is
# intended to be modified and sysmo_usim_write_...() to perform the actual
# modification task.

# Partial File tree:
# The following tree is incomplete, it just contains the propritary files we
# need to perform the tasks implemented below:
#
# [MF 0x3F00]
#  |
#  +--[DF_AUTH 0x7FCC]
#  |   |
#  |   +--[EF_AUTH 0x6F00]
#  |   |
#  |   +--[EF_MLNGC 0x6F01]
#  |
#  +--[DF_GSM 0x7F20]
#      |
#      +--[EF_OPC 0x00F7]
#      |
#      +--[EF_KI 0x00FF]
#      |
#      +--[EF_IMSI 0x6F07]

import sys
from utils import *
from sysmo_usim import *

# Files (propritary)
SYSMO_USIMSJS1_EF_KI = [0x00, 0xFF]
SYSMO_USIMSJS1_EF_OPC = [0x00, 0xF7]
SYSMO_USIMSJS1_DF_AUTH = [0x7F, 0xCC] #FIXME: Manual does not mention name, just called it "DF_AUTH" might be wrong!
SYSMO_USIMSJS1_EF_AUTH = [0x6F, 0x00]
SYSMO_USIMSJS1_EF_MLNGC = [0x6F, 0x01]
SYSMO_USIMSJS1_EF_SQNC = [0x00, 0xFB] # ADF.USIM
SYSMO_USIMSJS1_EF_SQNA = [0x00, 0xFA] # ADF.USIM
SYSMO_USIMSJS1_EF_EFMLNG = [0x00, 0xFB] # ADF.USIM
SYSMO_USIMSJS1_EF_AC = [0x00, 0xFE] # ADF.USIM

# Authentication algorithms (See sysmousim.pdf cap. 8.5)
SYSMO_USIMSJS1_ALGO_MILENAGE = 0x01
SYSMO_USIMSJS1_ALGO_COMP12V1 = 0x03
SYSMO_USIMSJS1_ALGO_XOR2G = 0x04
SYSMO_USIMSJS1_ALGO_COMP128V2 = 0x06
SYSMO_USIMSJS1_ALGO_COMP128V3 = 0x07
SYSMO_USIMSJS1_ALGO_XOR3G = 0x08

# Application identifier
SYSMO_USIM_AID = [0xa0, 0x00, 0x00, 0x00, 0x87, 0x10, 0x02]

# Default content of record No.1 in EF.DIR
SYSMO_USIM_EF_DIR_REC_1_CONTENT = [0x61, 0x19, 0x4f, 0x10] + SYSMO_USIM_AID + \
	[0xff, 0xff, 0xff, 0xff, 0x89, 0x07, 0x09, 0x00, 0x00, 0x50, 0x05,
	 0x55, 0x53, 0x69, 0x6d, 0x31, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
	 0xff, 0xff, 0xff, 0xff, 0xff]

# Abstraction for the file structure of EF.MLNGC, which holds the
# parameters of the milenage authentication algorithm
class SYSMO_USIMSJS1_FILE_EF_MLNGC:
	# Default parameters, see also sysmousim-manual.pdf,
	# cap. 8.6 "Milenage Configuration (Ci/Ri)
	C1 = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
	C2 = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x01]
	C3 = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02]
	C4 = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x04]
	C5 = [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
		0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x08]
	R1 = 0x40
	R2 = 0x00
	R3 = 0x20
	R4 = 0x40
	R5 = 0x60

	def __init__(self, content = None):
		if content == None:
			return
		if len(content) != 85:
			return
		self.C1 = content[0:16]
		self.C2 = content[16:32]
		self.C3 = content[32:48]
		self.C4 = content[48:64]
		self.C5 = content[64:80]
		self.R1 = content[80]
		self.R2 = content[81]
		self.R3 = content[82]
		self.R4 = content[83]
		self.R5 = content[84]

	def __str__(self):
		dump = "   C1: " + hexdump(self.C1) + "\n"
		dump += "   C2: " + hexdump(self.C2) + "\n"
		dump += "   C3: " + hexdump(self.C3) + "\n"
		dump += "   C4: " + hexdump(self.C4) + "\n"
		dump += "   C5: " + hexdump(self.C5) + "\n"
		dump += "   R1: " + str(hex(self.R1)) + "\n"
		dump += "   R2: " + str(hex(self.R2)) + "\n"
		dump += "   R3: " + str(hex(self.R3)) + "\n"
		dump += "   R4: " + str(hex(self.R4)) + "\n"
		dump += "   R5: " + str(hex(self.R5))
		return dump

	def encode(self):
		out = self.C1 + self.C2 + self.C3 + self.C4 + self.C5
		out += [self.R1, self.R2, self.R3, self.R4, self.R5]
		return out


class SYSMO_USIMSJS1_FILE_EF_SQNC:
	# Default parameters
	ind_size_bits = 5
	sqn_check_enabled = True
	sqn_age_limit_enabled = False
	sqn_max_delta_enabled = True
	sqnms_offset = 0
	max_delta = 2**28 << ind_size_bits
	age_limit = 2**28 << ind_size_bits

	def __init__(self, content = None):
		if content == None:
			return
		if len(content) != 15:
			raise ValueError("unexpected length of %u bytes", len(content))
		self.ind_size_bits = content[0] & 0xf
		self.sqn_check_enabled = bool(content[0] & 0x10)
		self.sqn_age_limit_enabled = bool(content[0] & 0x20)
		self.sqn_max_delta_enabled = bool(content[0] & 0x40)
		self.sqnms_offset = list_to_int(content[1:3])/6
		self.max_delta = list_to_int(content[3:9]) >> self.ind_size_bits
		self.age_limit = list_to_int(content[9:15]) >> self.ind_size_bits

	def __str__(self):
		pfx = "   "
		dump = ""

		dump += "%sIND (bits): %u\n" % (pfx, self.ind_size_bits)
		dump += "%sSQN Check enabled: %u\n" % (pfx, self.sqn_check_enabled)
		dump += "%sSQN Age Limit enabled: %u\n" % (pfx, self.sqn_age_limit_enabled)
		dump += "%sSQN Max Delta enabled: %u\n" % (pfx, self.sqn_max_delta_enabled)
		dump += "%sSQNms Offset (into SQN array): %u\n" % (pfx, self.sqnms_offset)
		dump += "%sMax Delta: %u\n" % (pfx, self.max_delta)
		dump += "%sAge Limit: %u\n" % (pfx, self.age_limit)
		return dump

	def encode(self):
		out = list(range(0, 3))
		out[0] = self.ind_size_bits & 0x0f
		if self.sqn_check_enabled:
			out[0] |= 0x10
		if self.sqn_age_limit_enabled:
			out[0] |= 0x20
		if self.sqn_max_delta_enabled:
			out[0] |= 0x40
		out[1] = (self.sqnms_offset*6) & 0xff
		out[2] = (self.sqnms_offset*6) >> 8
		out += int_to_list(self.max_delta, 6)
		out += int_to_list(self.age_limit, 6)
		return out

class SYSMO_USIMSJS1_FILE_EF_SQNA:
	seq_array = []

	def __init__(self, content, ind = 5):
		if content == None:
			for i in range(0, 2**ind):
				self.seq_array.append(0)
			return
		if len(content) != 6*(2**ind):
			raise ValueError("unexpected length of %u bytes", len(content))
		# read in the SEQ array
		for i in range(0, 2**ind):
			offset = 6*i;
			self.seq_array.append(list_to_int(content[offset:offset+6]))

	def __str__(self):
		pfx = "   "
		dump = ""
		for i in range(len(self.seq_array)):
			dump += "%sSEQ[%03d]: %u\n" % (pfx, i, self.seq_array[i])
		return dump

	def encode(self):
		out = []
		for i in self.seq_array:
			out += int_to_list(i, 6)
		return out

sysmo_usim_algorithms = (
		(1, 'MILENAGE'),
		(3, 'COMP128v1'),
		(4, 'XOR-2G'),
		(5, 'GBA'),
		(6, 'COMP128v2'),
		(7, 'COMP128v3'),
		(8, 'XOR-3G'),
		(9, 'CIS-B'),
	)

sysmo_usim_opcmodes = (
		(0, 'OP'),
		(1, 'OPc'),
	)


class Sysmo_usim_sjs1(Sysmo_usim):

	def __init__(self):
		Sysmo_usim.__init__(self, "3B 9F 96 80 1F C7 80 31 A0 73 BE 21 13 67 43 20 07 18 00 00 01 A5")


	# Show the enable status of the USIM application (app is enabled or disabled?)
	def show_sim_mode(self):
		print("Reading SIM-Mode...")
		self._init()

		print(" * Reading...")
		self.sim.select(GSM_USIM_EF_DIR)
		res = self.sim.read_record(0x26, rec_no = 1)

		print(" * Current status of Record No. 1 in EF.DIR:")
		print("   " + hexdump(res.apdu))

		if hexdump(SYSMO_USIM_AID) in hexdump(res.apdu):
			print("   ==> USIM application enabled")
		else:
			print("   ==> USIM application disabled")
		print("")


	# Show the enable status of the USIM application (app is enabled or disabled?)
	def write_sim_mode(self, usim_enabled = True):
		print("Programming SIM-Mode...")
		self._init()

		if usim_enabled:
			new_record = SYSMO_USIM_EF_DIR_REC_1_CONTENT
		else:
			new_record = [0xFF] * len(SYSMO_USIM_EF_DIR_REC_1_CONTENT)

		print(" * New status of Record No.1 in EF.DIR:")
		print("   " + hexdump(new_record))
		if hexdump(SYSMO_USIM_AID) in hexdump(new_record):
			print("   ==> USIM application enabled")
		else:
			print("   ==> USIM application disabled")

		print(" * Programming...")
		self.sim.select(GSM_USIM_EF_DIR)
		self.sim.update_record(new_record, rec_no = 1)
		print("")


	# Show current athentication parameters
	# (Which algorithim is used for which rat?)
	def show_auth_params(self):
		print("Reading Authentication parameters...")
		self._init()

		print(" * Reading...")
		self.sim.select(SYSMO_USIMSJS1_DF_AUTH)
		self.sim.select(SYSMO_USIMSJS1_EF_AUTH)
		res = self._read_binary(0x02)

		algo_2g, algo_3g = res.apdu[:2]

		print(" * Current algorithm setting:")
		print("   2G: %d=%s" % (algo_2g, id_to_str(sysmo_usim_algorithms, algo_2g)))
		print("   3G: %d=%s" % (algo_3g, id_to_str(sysmo_usim_algorithms, algo_3g)))
		print("")


	# Program new authentication parameters
	def write_auth_params(self, algo_2g_str, algo_3g_str):
		print("Programming Authentication parameters...")
		self._init()

		if algo_2g_str.isdigit():
			algo_2g = int(algo_2g_str)
		else:
			algo_2g = str_to_id(sysmo_usim_algorithms, algo_2g_str)

		if algo_3g_str.isdigit():
			algo_3g = int(algo_3g_str)
		else:
			algo_3g = str_to_id(sysmo_usim_algorithms, algo_3g_str)

		print(" * New algorithm setting:")
		print("   2G: %d=%s" % (algo_2g, id_to_str(sysmo_usim_algorithms, algo_2g)))
		print("   3G: %d=%s" % (algo_3g, id_to_str(sysmo_usim_algorithms, algo_3g)))

		print(" * Programming...")
		self.sim.select(SYSMO_USIMSJS1_DF_AUTH)
		self.sim.select(SYSMO_USIMSJS1_EF_AUTH)
		self.sim.update_binary([algo_2g,algo_3g])
		print("")


	# Show current milenage parameters
	def show_milenage_params(self):
		print("Reading Milenage parameters...")
		self._init()

		self.sim.select(SYSMO_USIMSJS1_DF_AUTH)
		self.sim.select(SYSMO_USIMSJS1_EF_MLNGC)

		print(" * Reading...")
		res = self._read_binary(85)
		ef_mlngc = SYSMO_USIMSJS1_FILE_EF_MLNGC(res.apdu)

		print(" * Current Milenage Parameters in (EF.MLNGC):")
		print(str(ef_mlngc))
		print("")


	# Write new milenage parameters
	def write_milenage_params(self, params):
		print("Programming Milenage parameters...")
		self._init()

		print(" * New Milenage Parameters for (EF.MLNGC):")
		ef_mlngc = SYSMO_USIMSJS1_FILE_EF_MLNGC(params)
		print(str(ef_mlngc))

		self.sim.select(SYSMO_USIMSJS1_DF_AUTH)
		self.sim.select(SYSMO_USIMSJS1_EF_MLNGC)

		print(" * Programming...")
		self.sim.update_binary(ef_mlngc.encode())
		print("")


	def __get_auth_counter(self):
		self.sim.select(SYSMO_USIMSJS1_EF_AC)
		res = self._read_binary(4, offset=0)
		ctr = list_to_int(res.apdu[0:4])
		if ctr == 0:
			return "LOCKED"
		elif ctr == 0xFFFFFFFF:
			return "DISABLED"
		else:
			return ctr


	def __set_auth_counter(self, ctr):
		if ctr == "LOCKED":
			ctr = 0
		elif ctr == "DISABLED":
			ctr = 0xFFFFFFFF
		data = int_to_list(ctr, 4)
		self.sim.select(SYSMO_USIMSJS1_EF_AC)
		res = self.sim.update_binary(data, offset=0)
		if ctr == 0:
			return "LOCKED"
		elif ctr == 0xFFFFFFFF:
			return "DISABLED"
		else:
			return ctr


	# Show current milenage SQN parameters
	def show_milenage_sqn_params(self):
		print("Reading Milenage Sequence parameters...")
		self._init()

		self.sim.card.SELECT_ADF_USIM()
		self.sim.select(SYSMO_USIMSJS1_EF_SQNC)

		res = self._read_binary(15, offset = 0)
		ef_sqnc = SYSMO_USIMSJS1_FILE_EF_SQNC(res.apdu)
		print(" * Current SQN Configuration:")
		print(str(ef_sqnc))

		# SQN Array
		ind_pow = 2**ef_sqnc.ind_size_bits
		self.sim.select(SYSMO_USIMSJS1_EF_SQNA)
		res = self._read_binary(ind_pow*6, offset=0)
		ef_sqna = SYSMO_USIMSJS1_FILE_EF_SQNA(res.apdu)
		print(" * Current SQN Array:")
		print(str(ef_sqna))

		auth_ctr = self.__get_auth_counter()
		print("* Authentication Counter: %s" % auth_ctr)
		print("")


	# Reset milenage SQN configuration
	def reset_milenage_sqn_params(self):
		print(" * Resetting SQN Configuration to defaults...")
		self._init()

		print(" * Resetting...")
		self.sim.card.SELECT_ADF_USIM()
		ef_sqnc = SYSMO_USIMSJS1_FILE_EF_SQNC(None)
		self.sim.select(SYSMO_USIMSJS1_EF_SQNC)
		res = self.sim.update_binary(ef_sqnc.encode())

		ef_sqna = SYSMO_USIMSJS1_FILE_EF_SQNA(None, ef_sqnc.ind_size_bits)
		self.sim.select(SYSMO_USIMSJS1_EF_SQNA)
		res = self.sim.update_binary(ef_sqna.encode())

		self.__set_auth_counter("DISABLED")
		print("")


	# Show current OPc value
	def show_opc_params(self):
		print("Reading OP/c value...")
		self._init()

		print(" * Reading...")
		self.sim.card.SELECT_ADF_USIM()
		self.sim.select(SYSMO_USIMSJS1_EF_OPC)
		res = self._read_binary(17)

		mode_str = id_to_str(sysmo_usim_opcmodes, res.apdu[0])

		print(" * Current OP/OPc setting:")
		print("   %s: %s" % (mode_str, hexdump(res.apdu[1:])))
		print("")


	# Program new OPc value
	def write_opc_params(self, select, op):
		if select:
			print("Writing OPc value...")
		else:
			print("Writing OP value...")
		self._init()

		print(" * New OPc setting:")
		print("   %s: %s" % (id_to_str(sysmo_usim_opcmodes, select), hexdump(op)))

		self.sim.select(GSM_SIM_DF_GSM)
		self.sim.select(SYSMO_USIMSJS1_EF_OPC)

		print(" * Programming...")
		self.sim.update_binary([select] + op)
		print("")


	# Show current KI value
	def show_key_params(self):
		print("Reading KI value...")
		print(" * Reading...")
		self.sim.select(GSM_SIM_DF_GSM)
		self.sim.select(SYSMO_USIMSJS1_EF_KI)
		res = self._read_binary(16)

		print(" * Current KI setting:")
		print("   KI: " + hexdump(res.apdu))
		print("")


	# Program new KI value
	def write_key_params(self, ki):
		print("Writing KI value...")
		self._init()

		print(" * New KI setting:")
		print("   KI: " + hexdump(ki))

		self.sim.select(GSM_SIM_DF_GSM)
		self.sim.select(SYSMO_USIMSJS1_EF_KI)

		print(" * Programming...")
		self.sim.update_binary(ki)
		print("")