2017-08-21 23:09:05 +02:00
|
|
|
import socket
|
|
|
|
import os
|
|
|
|
import threading
|
2017-10-18 00:36:28 +02:00
|
|
|
import sys
|
|
|
|
import signal
|
2017-08-21 23:09:05 +02:00
|
|
|
from Crypto.Cipher import AES
|
|
|
|
|
|
|
|
class AuthCenter:
|
|
|
|
'''
|
|
|
|
Home Location Register (HLR) and Authentication Center (AuC) used for
|
|
|
|
for retrieving SIM/AKA values. This provides a UDP server that
|
|
|
|
hostapd can communicate with to obtain SIM values.
|
|
|
|
'''
|
|
|
|
def __init__(self, sock_path, config_file):
|
|
|
|
self._read_config(config_file)
|
|
|
|
self._socket = socket.socket(socket.AF_UNIX, socket.SOCK_DGRAM)
|
2017-10-18 00:36:28 +02:00
|
|
|
if os.path.exists(sock_path):
|
2017-08-21 23:09:05 +02:00
|
|
|
os.unlink(sock_path)
|
|
|
|
self._socket.bind(sock_path)
|
|
|
|
|
|
|
|
self._rxhandle = threading.Thread(target=self._rx_thread)
|
2018-01-18 19:49:37 +01:00
|
|
|
self._rxhandle.ready = threading.Event()
|
2017-08-21 23:09:05 +02:00
|
|
|
self._rxhandle.start()
|
|
|
|
|
2018-01-18 19:49:37 +01:00
|
|
|
# wait for rx thread to start
|
|
|
|
self._rxhandle.ready.wait()
|
|
|
|
|
2017-08-21 23:09:05 +02:00
|
|
|
def _rx_thread(self):
|
2018-01-18 19:49:37 +01:00
|
|
|
self._rxhandle.ready.set()
|
2017-08-21 23:09:05 +02:00
|
|
|
while (True):
|
|
|
|
try:
|
|
|
|
data, addr = self._socket.recvfrom(1000)
|
|
|
|
data = data.decode('ascii')
|
|
|
|
resp = self._process_data(data)
|
2018-01-18 19:49:37 +01:00
|
|
|
except OSError:
|
|
|
|
break
|
2017-08-21 23:09:05 +02:00
|
|
|
except:
|
2017-12-13 23:29:15 +01:00
|
|
|
print("Exception:", sys.exc_info()[0])
|
2018-01-18 19:49:37 +01:00
|
|
|
break
|
2017-08-21 23:09:05 +02:00
|
|
|
if resp:
|
|
|
|
self._socket.sendto(bytearray(resp, 'UTF-8'), addr)
|
|
|
|
|
|
|
|
def _read_config(self, file):
|
|
|
|
self._database = {}
|
|
|
|
with open(file) as f:
|
|
|
|
for line in f:
|
|
|
|
if line[0] == '#':
|
|
|
|
continue
|
|
|
|
else:
|
2017-12-13 23:29:15 +01:00
|
|
|
data = line.strip('\n').split(':')
|
2017-12-13 23:29:14 +01:00
|
|
|
self._database[data[0]] = data[1:]
|
2017-08-21 23:09:05 +02:00
|
|
|
|
|
|
|
def _process_data(self, data):
|
|
|
|
if data[:12] == "SIM-REQ-AUTH":
|
|
|
|
# SIM requests just return the stored values for the IMSI
|
|
|
|
imsi, num_chals = data[13:].split(' ')
|
2017-10-18 00:36:28 +02:00
|
|
|
if not imsi or not num_chals:
|
|
|
|
return "ERROR"
|
|
|
|
|
|
|
|
data = self._database.get(imsi, None)
|
|
|
|
if not data:
|
|
|
|
return "ERROR"
|
2017-08-21 23:09:05 +02:00
|
|
|
|
|
|
|
response = "SIM-RESP-AUTH %s" % imsi
|
2017-12-13 23:29:14 +01:00
|
|
|
response += (' ' + ':'.join(data))*int(num_chals)
|
2017-08-21 23:09:05 +02:00
|
|
|
|
|
|
|
return response
|
2017-08-21 23:09:07 +02:00
|
|
|
elif data[:12] == "AKA-REQ-AUTH":
|
|
|
|
# AKA requests must compute the milenage parameters for the IMSI
|
|
|
|
imsi = data.split(' ')[1]
|
2017-10-18 00:36:28 +02:00
|
|
|
data = self._database.get(imsi, None)
|
|
|
|
if not data:
|
|
|
|
return "ERROR"
|
|
|
|
|
|
|
|
# make sure this is an AKA entry
|
2017-12-13 23:29:14 +01:00
|
|
|
if len(data) < 4:
|
2017-10-18 00:36:28 +02:00
|
|
|
return "ERROR"
|
2017-08-21 23:09:07 +02:00
|
|
|
|
2017-12-13 23:29:14 +01:00
|
|
|
k, opc, amf, sqn = data
|
2017-08-21 23:09:07 +02:00
|
|
|
|
|
|
|
rand = self._bytetostring(os.urandom(16))
|
|
|
|
|
|
|
|
response = "AKA-RESP-AUTH %s " % imsi
|
|
|
|
|
|
|
|
return response + self._get_milenage(opc, k, rand, sqn, amf)
|
2017-12-13 23:29:14 +01:00
|
|
|
elif data[:8] == "AKA-AUTS":
|
|
|
|
# sync error, parse out SQN and reset in database
|
|
|
|
imsi, auts, rand = data[9:].split(' ')
|
|
|
|
|
|
|
|
entry = self._database.get(imsi, None)
|
|
|
|
if not entry:
|
|
|
|
return "ERROR"
|
|
|
|
|
|
|
|
# make sure this is an AKA entry
|
|
|
|
if len(entry) < 4:
|
|
|
|
return "ERROR"
|
|
|
|
|
|
|
|
k, opc, amf, sqn = entry
|
|
|
|
|
|
|
|
# calculate/set new sequence number
|
|
|
|
entry[3] = self._resync_autn(opc, k, rand, auts)
|
|
|
|
self._database[imsi] = entry
|
|
|
|
|
|
|
|
return None
|
2017-08-21 23:09:07 +02:00
|
|
|
|
|
|
|
def _bytetostring(self, b):
|
|
|
|
return ''.join(format(x, '02x') for x in b)
|
|
|
|
|
|
|
|
def _xor(self, a, b):
|
|
|
|
ret = bytearray(16)
|
|
|
|
for i in range(len(a)):
|
|
|
|
ret[i] = a[i] ^ b[i]
|
|
|
|
return ret
|
|
|
|
|
2017-12-13 23:29:14 +01:00
|
|
|
def _resync_autn(self, opc, k, rand, auts):
|
|
|
|
opc = bytearray.fromhex(opc)
|
|
|
|
k = bytearray.fromhex(k)
|
|
|
|
rand = bytearray.fromhex(rand)
|
|
|
|
auts = bytearray.fromhex(auts)
|
|
|
|
new_sqn = bytearray(6)
|
|
|
|
ak_star = bytearray(6)
|
|
|
|
|
|
|
|
temp = self._xor(rand, opc)
|
|
|
|
aes1 = AES.new(bytes(k), AES.MODE_ECB)
|
|
|
|
temp = aes1.encrypt(bytes(temp))
|
|
|
|
temp = bytearray(temp)
|
|
|
|
|
|
|
|
out5 = bytearray(16)
|
|
|
|
for i in range(16):
|
|
|
|
out5[(i + 4) % 16] = temp[i] ^ opc[i];
|
|
|
|
|
|
|
|
out5[15] ^= 8
|
|
|
|
|
|
|
|
aes2 = AES.new(bytes(k), AES.MODE_ECB)
|
|
|
|
out5 = aes2.encrypt(bytes(out5))
|
|
|
|
out5 = bytearray(out5)
|
|
|
|
|
|
|
|
for i in range(6):
|
|
|
|
ak_star[i] = out5[i] ^ opc[i]
|
|
|
|
|
|
|
|
for i in range(6):
|
|
|
|
new_sqn[i] = auts[i] ^ ak_star[i]
|
|
|
|
|
|
|
|
return self._bytetostring(new_sqn)
|
|
|
|
|
2017-08-21 23:09:07 +02:00
|
|
|
def _get_milenage(self, opc, k, rand, sqn, amf):
|
|
|
|
'''
|
|
|
|
Computes milenage values from OPc, K, RAND, SQN and AMF
|
|
|
|
Returns a concatenated list (RAND + AUTN + IK + CK + RES) that
|
|
|
|
will be sent back as the response to the client (hostapd). This
|
|
|
|
is a python re-write of the function eap_aka_get_milenage() from
|
|
|
|
src/simutil.c
|
|
|
|
'''
|
|
|
|
opc = bytearray.fromhex(opc)
|
|
|
|
k = bytearray.fromhex(k)
|
|
|
|
# rand gets returned, so it should be left as a hex string
|
|
|
|
_rand = bytearray.fromhex(rand)
|
|
|
|
sqn = bytearray.fromhex(sqn)
|
|
|
|
amf = bytearray.fromhex(amf)
|
|
|
|
|
|
|
|
aes1 = AES.new(bytes(k), AES.MODE_ECB)
|
|
|
|
tmp1 = self._xor(_rand, opc)
|
|
|
|
tmp1 = aes1.encrypt(bytes(tmp1))
|
|
|
|
tmp1 = bytearray(tmp1)
|
|
|
|
|
|
|
|
tmp2 = bytearray()
|
|
|
|
tmp2[0:6] = sqn
|
|
|
|
tmp2[6:2] = amf
|
|
|
|
tmp2[9:6] = sqn
|
|
|
|
tmp2[15:2] = amf
|
|
|
|
|
|
|
|
tmp3 = bytearray(16)
|
|
|
|
for i in range(len(tmp1)):
|
|
|
|
tmp3[(i + 8) % 16] = tmp2[i] ^ opc[i]
|
|
|
|
|
|
|
|
tmp3 = self._xor(tmp3, tmp1)
|
|
|
|
|
|
|
|
aes2 = AES.new(bytes(k), AES.MODE_ECB)
|
|
|
|
tmp1 = aes2.encrypt(bytes(tmp3))
|
|
|
|
tmp1 = bytearray(tmp1)
|
|
|
|
|
|
|
|
tmp1 = self._xor(tmp1, opc)
|
2017-12-13 23:29:14 +01:00
|
|
|
maca = self._bytetostring(tmp1[0:8])
|
2017-08-21 23:09:07 +02:00
|
|
|
|
|
|
|
tmp1 = self._xor(_rand, opc)
|
|
|
|
aes3 = AES.new(bytes(k), AES.MODE_ECB)
|
|
|
|
tmp2 = aes3.encrypt(bytes(tmp1))
|
|
|
|
tmp2 = bytearray(tmp2)
|
|
|
|
|
|
|
|
tmp1 = self._xor(tmp2, opc)
|
|
|
|
tmp1[15] ^= 1
|
|
|
|
|
|
|
|
aes4 = AES.new(bytes(k), AES.MODE_ECB)
|
|
|
|
tmp3 = aes4.encrypt(bytes(tmp1))
|
|
|
|
tmp3 = bytearray(tmp3)
|
|
|
|
|
|
|
|
tmp3 = self._xor(tmp3, opc)
|
|
|
|
|
|
|
|
res = self._bytetostring(tmp3[8:16])
|
|
|
|
ak = self._bytetostring(tmp3[0:6])
|
|
|
|
|
|
|
|
for i in range(len(tmp1)):
|
|
|
|
tmp1[(i + 12) % 16] = tmp2[i] ^ opc[i]
|
|
|
|
|
|
|
|
tmp1[15] ^= 1 << 1
|
|
|
|
aes5 = AES.new(bytes(k), AES.MODE_ECB)
|
|
|
|
tmp1 = aes5.encrypt(bytes(tmp1))
|
|
|
|
tmp1 = bytearray(tmp1)
|
|
|
|
|
|
|
|
tmp1 = self._xor(tmp1, opc)
|
|
|
|
ck = self._bytetostring(tmp1)
|
|
|
|
|
|
|
|
for i in range(len(tmp1)):
|
|
|
|
tmp1[(i + 8) % 16] = tmp2[i] ^ opc[i]
|
|
|
|
|
|
|
|
tmp1[15] ^= 1 << 2
|
|
|
|
aes6 = AES.new(bytes(k), AES.MODE_ECB)
|
|
|
|
tmp1 = aes6.encrypt(bytes(tmp1))
|
|
|
|
tmp1 = bytearray(tmp1)
|
|
|
|
tmp1 = self._xor(tmp1, opc)
|
|
|
|
ik = self._bytetostring(tmp1)
|
|
|
|
|
|
|
|
tmp1 = bytearray.fromhex(ak)
|
|
|
|
autn = bytearray(6)
|
|
|
|
for i in range(0, 6):
|
|
|
|
autn[i] = sqn[i] ^ tmp1[i]
|
|
|
|
|
|
|
|
autn[6:2] = amf
|
|
|
|
autn[8:8] = bytearray.fromhex(maca)[0:8]
|
|
|
|
|
|
|
|
autn = self._bytetostring(autn)
|
|
|
|
|
|
|
|
return rand + ' ' + autn + ' ' + ik + ' ' + ck + ' ' + res
|
2017-08-21 23:09:05 +02:00
|
|
|
|
|
|
|
def stop(self):
|
|
|
|
'''
|
|
|
|
Stop the Authentication server and close the socket
|
|
|
|
'''
|
2018-01-18 19:49:37 +01:00
|
|
|
self._socket.shutdown(socket.SHUT_RDWR)
|
2017-08-21 23:09:05 +02:00
|
|
|
self._socket.close()
|
2018-01-18 19:49:37 +01:00
|
|
|
self._rxhandle.join()
|
2017-10-18 00:36:28 +02:00
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
'''
|
|
|
|
This will run in a stand-alone mode for testing
|
|
|
|
'''
|
|
|
|
if len(sys.argv) < 3:
|
|
|
|
print('Usage: ./hlrauc.py <sock_path> <config>')
|
|
|
|
sys.exit()
|
|
|
|
|
|
|
|
hlrauc = AuthCenter(sys.argv[1], sys.argv[2])
|
|
|
|
|
|
|
|
def signal_handler(signal, frame):
|
|
|
|
print('Exiting...')
|
|
|
|
hlrauc.stop()
|
|
|
|
sys.exit()
|
|
|
|
|
|
|
|
signal.signal(signal.SIGINT, signal_handler)
|
|
|
|
|
|
|
|
signal.pause()
|