Our Android application from Chapter 6, Creating an Android Client, can operate over public networks (public from the point of view of your home network). This means that basically anyone can connect to your socket, if the connection is currently not busy and the person knows your IP address and the socket port you decide to use. We have currently not implemented any type of security enforcement; we only drop the connection from the server side if we receive an unclear response to our Hello Message. However, since we do send the initial data packet in a clearly readable form, the person receiving the data can deduce that our server operates with a clear text protocol. If this is a malicious attempt, reverse engineering the protocol from this point onwards is not too difficult, if time and energy is put to the task.
Of course, all socket communications work on streams, and there are several types of security measures that you can take depending on the level of security you feel your connection requires.
You are basically only limited by your imagination and the type of security you can implement. Let me give you some ideas.
One of the simplest things that you could do is to reverse the initial connection procedure. Instead of sending a welcome message, the server would never respond to a new socket, but immediately wait for a valid input. You could easily implement a protocol-version-checking sequence, so that if this functionality is required, a client has to initiate it himself.
In addition to this, a connection would be silently dropped, if input that is not valid is detected. Then a temporary counter would be incremented from this particular IP address, and if, for example, three consecutive incorrect sequence initiations follow, a ban would be set for this IP address for a specified length of time. Of course, all these failed connection attempts need to be logged and perhaps an e-mail should be sent to the administrator about it.
Much stronger security is provided by requiring all clients to authenticate themselves when they connect. But since sending a password without encryption over the network is not a healthy habit, we will also require encryption. Valid login and password combinations will be held on a file on the server, and this way only server administrator can manage logins. During the initial connection establishment, a valid encrypted login-password sequence must be sent to the server, else again the server initiates its cold shoulder disconnection sequence. There are several ways to encrypt and decrypt data in Python, and we will choose 128-bit AES encryption with preshared keys, as that encryption is secure, and available in Android by default as well.
To use encryption on our Beagle, we will need to download a library for this purpose from the Internet. So, make sure you are connected (and you have done the automatic date configuration part from Chapter 2, Input and Output) and type in the following code:
root@beaglebone:~# pip install pycrypto
This will install the necessary libraries to enable encryption on our Python code in Beagle. Next, we should create a file that will hold the password data. For now, we will not encrypt it, but after reading this chapter, we're sure you will have a good idea how to encrypt this data, if you wish to do so. Create a file called valid_passwords.txt
, and in here, add each user to a separate line in the username:password
format:
root@beaglebone:~/ch7# cat valid_passwords.txt jlumme:mypassword reader:anotherpassword
Now, we are ready to start modifying the actual code.
Let's start implementing the first two security features to beagle_server.py
we have talked about so far. First, will need to import an AES cipher from the Crypto
library:
from Crypto.Cipher import AES
And then we will need some constants and a lambda definition:
password_filename = "valid_passwords.txt"
# Encryption related definitions
BS = 16 #Our key size (16 bytes -> 128bit encryption)
SECRET_KEY="BeagleHomeAutoma" # Keep it 16 characters if you change
# The data needs to (un-)padded to the BS borders
pad = lambda s: s + (BS - len(s) % BS) * chr(BS - len(s) % BS)
unpad = lambda s : s[0:-ord(s[-1])]
You can see here that we first defined a filename that will hold a login-password combination (we will leave it as a clear text file for now). Then we define the variable BS
; this defines our block size for the encryption. You could use 16, 24, or 32 here for 128-, 192-, or 256-bit encryption respectively. We also defined our preshared secret encryption key SECRET_KEY
that will be known to both the server and the client, and encryption and decryption will use this key. If you change it, keep in mind that you have to give it a proper block size, otherwise the crypto functions will fail to operate.
Then we defined two lambda functions, pad
and unpad
. Lambda functions are nameless functions that can be defined at any time and always return a value. Here we defined two functions that will automatically append or remove padding from an input if its size is not equal to the border defined by BS
.
Now let's define a function that will read the password file and compare whether the supplied login is valid:
def compare_to_valid_logins(username, password): pw_list = open (password_filename, 'r').read().splitlines() for line in pw_list: uname = line[:line.index(":")] pword = line[line.index(":")+1:] # If username and password match, return 1 if uname == username and pword == password: return 1 # If no successful login combination found return 0
We read the password file line by line, and compared it with the function arguments to see if a matching combination is found. The login-password is supposed to be sent separated by the ":" character. Now we will need to implement the actual message decryption function. It is somewhat similar to a normal handle_client_request()
function, but of course with some extras for handling the decryption:
def verify_password(cs): try: msg = cs.recv(4) # Retrieve the header, this is a blocking call header = unpack("!HH", msg) # Decode the mandatory headers # Verify that sender sent the PASSWORD header if header[0] == BP.MT_PASSWORD: # Check how much date is left in the stream remaining_size = int(header[1]) print "Encrypted package is %d bytes" % remaining_size # Read the encrypted package encrypted_data = cs.recv(remaining_size)
So far everything is the same as when reading a message from a client. But then we need to decrypt the secret data:
iv = encrypted_data[:16] # Initialize our deciphering key key = AES.new(SECRET_KEY, AES.MODE_CBC,IV=iv) # Rest of the packet is supposed to be login data decrypted = key.decrypt(encrypted_data[16:]) login_details = unpad(decrypted) # Rmove possible padding
You can see that the client has appended the Initialization Vector (IV) to the first 16 bytes of the message. An IV is used to initialize the cipher, and it can be chosen randomly (again, as long as it is 16 bytes). After that we call the cipher to create us a key for decryption with the SECRET_KEY
and IV
information. Lastly we decrypt the data, and remove the possible padding using the unpad
lambda function.
After this we hold the login details in the login_details
variable. We will verify them with the function that we defined before, as follows:
# Verify that the format is correct if ":" in received_login: username = received_login[:received_login.index(":")] password = received_login[received_login.index(":")+1:] return compare_to_valid_logins(username, password) else: return 0 # Any error in the procedure, we just disconnect the client except Exception, e: print "Something went wrong:" print e return 0 # Drop the connection
Since it's mandatory for our client to authenticate itself, we also have to be sure that the authentication data is available. During startup, we have to check for the existence of the password file. In the main
function, right in the very beginning, add the following code:
#Check that password file has been defined: try: with open(password_filename): pass except IOError: print "Password file [%s] not found, exiting" % password_filename sys.exit(1)
Lastly, we should add a call to the verify_password()
method in our main
function, right after the client connects to the server:
client = server_socket.wait_for_client(srv) # blocking!
result = verify_password(client)
if not result == 1:
print "Something wrong, drop the socket"
client.close()
continue
Now the server will always expect an encrypted login from a newly connecting client. If something doesn't occur the way the server expects it to, it will just drop the connection. Next, let's add support for integrating this with our Android client.
We do not need too many modifications to MainActivity.java
for now. All the encryption will be handled in the networking part of our code. On the UI side, we will only query the user for a username and password during the connection attempt and pass that information to our network thread. So, first we add a couple of new variables as follows:
private String username = ""; private String password = "";
To get the login details from the user, we will not modify the existing UI, but instead add a new login Alert
popup that is shown when the user clicks on the Connect button. For this purpose, we need to create a new UI layout definition file called password_query.xml
. We will create it by navigating to File | New | New Android XML file and add it under resources. To this file, add the following definition:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <TextView android:layout_height="wrap_content" android:layout_width="wrap_content" android:id="@+id/login_username_textview" android:text="@string/login_username" android:textStyle="bold" /> <EditText android:layout_height="wrap_content" android:layout_width="match_parent" android:id="@+id/login_username_edittext" android:inputType="text" /> <TextView android:layout_height="wrap_content" android:layout_width="wrap_content" android:id="@+id/login_password_textview" android:text="@string/login_password" android:textStyle="bold" /> <EditText android:layout_height="wrap_content" android:layout_width="match_parent" android:id="@+id/login_password_edittext" android:inputType="textPassword" /> </LinearLayout>
Now we can implement our loginDialog
function as follows:
public AlertDialogloginDialog(Context c, String message) { //Inflate the login query window from resources LayoutInflater factory = LayoutInflater.from(c); final View textEntryView = factory.inflate(R.layout.password_query, null); //Set message and title, and button details AlertDialog.Builder alert = new AlertDialog.Builder(c); alert.setTitle("Login"); alert.setMessage(message); alert.setView(textEntryView);
Here we retrieve the layout information from the resources, and start by creating a new alert dialog with the information supplied to the function. Next, we will add button handlers. First, we will add the "confirm" button as shown:
alert.setPositiveButton("Login", \ new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, int whichButton) { final EditText usernameInput = (EditText) textEntryView.findViewById(R.id.login_username_edittext); final EditText passwordInput = (EditText) textEntryView.findViewById(R.id.login_password_edittext); //Get the text user entered username = usernameInput.getText().toString(); password = passwordInput.getText().toString(); new Thread(new Runnable() { public void run() { nt = new NetworkTask(SERVER_ADDR, SERVER_PORT, handler, username + ":" + password); nt.run(); } }).start(); } });
We add an onClickListener
event for the login query window when the user clicks on the PositiveButton
. It will fetch the data from the EditText
fields and pass that information to NetworkTask
(which we have moved here from the original uiEventHandler
function). Next, we will add another onClickListener
event for the NegativeButton
as follows:
//If user chooses to cancel alert.setNegativeButton("Cancel", new DialogInterface.OnClickListener() { public void onClick(DialogInterface dialog, intwhichButton) { // Do nothing } });
Now all that is left is to return the created dialog:
return alert.create();
Now we will modify the uiEventHandler
function. We will remove the NetworkTask
starting code from here, and instead add the following code to show the alert dialog that we just created:
public void uiEventHandler(View v) {
//Connect button pressed, and we are not connected -> connect
if (v == connectButton&& !connectedToServer ) {
AlertDialog a = loginDialog(this, "Login details:");
a.show();
}
//Connect button pressed, and we're connected -> disconnect
...
Our UI can now handle user login prompts, and we can start thinking about the network thread implementation. For this purpose, we will need to define the following new constants in the BeagleProtocol.java
file:
public final static short MT_PASSWORD = 18; public final static short LOGIN_FAILED = 66; public final static short ENCRYPTION_FAILED = 67;
Next, we need to add the piece of code that encrypts our login details and sends them to the server for validation in NetworkTask.java
. First, we need to add the following new variables:
private String loginUsernamePassword = ""; //Encryption related private IvParameterSpecivspec; private SecretKeySpeckeyspec; private Cipher cipher; private String SecretKey = "BeagleHomeAutoma"; //16 bytes -> 128bit encryption
We also need to modify our constructor as follows:
NetworkTask(String address, intport,Handler handler, String login) { serverAddress = address; serverPort = port; parent = handler; loginUsernamePassword = login; alive = true; //Set to false to end the thread during disconnection }
Add a new function called sendEncryptedLoginDetails
. This function will be responsible for encryption, and send the encrypted data to the server. Since our server does not reply to invalid logins, all we can do is proceed as follows, and evaluate the connection state later:
public intsendEncryptedLoginDetails() { //First initialize the random IV, and retrieve AES key spec SecureRandomrnd = new SecureRandom(); byte [] ivbytes = rnd.generateSeed(16); ivspec = new IvParameterSpec(ivbytes); keyspec = new SecretKeySpec(SecretKey.getBytes(), "AES"); byte[] encrypted = null;
Here we use the SecureRandom
class to generate 16 random bytes for our IV. These bytes will be used as the "salt" for the encryption. Then we create the encryption key that is preshared between the client and server (String SecretKey
). After this we initialize the cipher, and encrypt our message as follows:
//Define and initialize the cipher try { cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); cipher.init(Cipher.ENCRYPT_MODE, keyspec, ivspec); //Perform the encryption encrypted = cipher.doFinal(loginUsernamePassword.getBytes()); } catch (Exception enc) { debug("Encryption failed"); enc.printStackTrace(); return 1; }
We first initialize the cipher by defining it with the desired specifics of the encryption, and then encrypt the loginUserNamePassword
String
. In the actual code, we separate the different try-catch blocks to identify which part of the code failed, so that the errors can be properly acted upon. But here, we have combined it with one catch-all call to save space.
Now that we have the encrypted bytes ready we can create our data package and send it, as shown in the following lines of code:
int enc_len = encrypted.length; int iv_len = ivbytes.length; byte[] pw = new byte[4 + iv_len + enc_len]; //Construct the message pw[0] = (byte)((BeagleProtocol.MT_PASSWORD>> 8) & 0xff); pw[1] = (byte)(BeagleProtocol.MT_PASSWORD&0xff); pw[2] = (byte)((enc_len + iv_len>> 8) & 0xff); pw[3] = (byte)(enc_len + iv_len& 0xff); //Copy the IV to the outgoing packet System.arraycopy(ivbytes,0,pw,4 ,iv_len); //Copy the encrypted data to outgoing packet System.arraycopy(encrypted, 0, pw, 4 + iv_len, enc_len); //Send the data try { os.write(pw); } catch (IOExceptionioe) { debug("Sending password has failed"); ioe.printStackTrace(); } //Cleanup rnd = null; ivspec = null; pw = null; return 0; //Success }
Now that our encrypted login function is complete, all that is left is to add a proper place to call it in the run()
method.
This call is placed after we have initialized our socket and data streams, as follows:
os = new DataOutputStream(socket.getOutputStream()); //Send the password information to the server: int res = sendEncryptedLoginDetails(); if (res != 0) { debug("Encryption failed..."); //In reality, we cannot really do much here. //Something wrong with selected encryption settings } //Read the initial welcome message byte[] readArray = new byte[6]; int howmany = is.read(readArray, 0, 6); //If our login fails, server will just drop the socket if (howmany == -1) { debug("Stream has closed. Something wrong with login ?"); //Inform the UI about login failure Message msg = new Message(); msg.what = BeagleProtocol.LOGIN_FAILED; msg.obj = null; //send the message parent.handleMessage(msg); is.close(); os.close(); socket.close(); return; }
After we initialize our streams, we call the sendEncryptedLoginDetails
function to identify us with the server along with the details we provided in the previous login query. After that, all we can do is check if the server is still sending us information (try to read the protocol version). If the socket has been dropped (the read
stream returns -1
), it means that the server has prevented our login, and we have to handle it. We inform the UI via our Handler
class, close the streams, and return from this function, thus ending the life of this thread.
With the previous code, you are now securely transmitting your login details over the network.
In the previous section, you saw how you can encrypt one message. The next step in beefing up the security would be to encrypt all of the communication. With the tips you saw earlier, this wouldn't be such a big hurdle; you will just have to modify the normal message sequence a bit. You could change the places of the "message request" and "message size" headers, so that only the incoming message size would be unencrypted, and everything else would always be encrypted.
The next step would be to also implement secure key exchange, so that the encryption would always be done with different keys. This will be getting into the advanced areas of cryptography and TCP/IP security, so we will leave it for a topic in another book.