Secure node encrypted communication. AES-128



  • Some time ago I made a custom servo-lock which is connected to MySensors network and can be locked/unlocked either manually, with button on a door knob or remotely on the Domoticz web page. Since this lock is located on one of my entrance doors I wanted it to be as secure as possible. Unfortunately neither Mysensors nor Domoticz doesn't provide reliable solution for this kind of appliance. As discussed here once you are on your local network you can easily get access to all sensible data. My idea was to make pin-code protected switch to open my lock remotely.
    0_1522605017828_mysensors_loc_domoticz.png
    Domoticz would collect presses on the selector switch and then send them as a string to a node, which will compare pin-code with the one saved in it's memory and unlock the door if it's correct. Good idea but not secure at all as long as string is transmitted as a plain text. For this reason I decided to use AES cipher to encrypt pin-code message and send it to a node.
    After some research (aka google hard) I wrote a python script that receives a pin-code as a parameter, encrypts it with AES key, encodes it with base64 and then sends via socket connection to an ethernet gateway of mysensors. To be able to use it with available controllers i.e. Domoticz or Openhab I wrote it this way:

    1. It receives these parameters: --node - obviously a node ID for message to be sent, --ip - IP address of the gateway, by default it's 127.0.0.1 to be used with a RaspberryPi gw, --port - gateway port (default 5003), --key - 16 bit AES key, (it is not secure to expose it on web interface, so it's better to be changed in the code), --msg - pin-code itself which has to be 4 bits long, but you can change it for example to 6 or 8 bits in code, the longer ones won't be practical.
    2. Then it have two modes. The first is used in case if you pass whole pin-code at once (4bits), then it encrypts and sends it. In case you are calling the script with Dototicz, for example, it can collect your pin-code byte by byte and then encrypt it.
    3. In this case it checks if there is a copy of itself running. In case it doesn't finds any it makes a fork, creates fifo file in /tmp folder and waits for 10 seconds for next bit to be collected through fifo.
    4. If it finds a copy of itself running it writes to a fifo one bit it received as command line argument and exits.
    5. When 4 bits are collected it adds 6 random bits before it and 6 random bits after, so the string is 16 bit long. Then it encrypts it as a single AES block and then encodes in base64, which makes 24 bit long message to be sent to a gateway like this 1;1;1;0;47;uKvGG7z440r/1pln4IJbNQ==

    Node receives message decodes it and then checks if it's correct.
    On Domoticz side I use dummy selector switch with characters from 1 to 0.
    0_1522607716698_mysensors_lock_domoticz2.png
    It reacts somewhat laggy (as everything in Domoticz) but after some delay it opens my lock. Also I use simple event script in Domoticz to switch selector switch back to "off" state after every change, so the last character of a pin-code won't show on page.

    Python code:

    #!/usr/bin/python3
    
    import os
    import sys
    import socket
    import time
    import base64
    from Crypto.Cipher import AES
    from Crypto import Random
    import argparse
    import errno
    from subprocess import check_output
    
    KEY = 'abcdefGHIJKLmnop'  # AES 16 bit key
    msg_len = 4  # Change to 6 or 8 for stronger pin-code
    fifo_path = "/tmp/mysencrypt.fifo"
    
    
    def first(msg):
        try:
            os.unlink(fifo_path)
        except OSError as err:
            if err.errno == errno.ENOENT:
                pass
            else:
                raise
    
        os.mkfifo(fifo_path)
        fifo = os.open(fifo_path,  os.O_RDONLY | os.O_NONBLOCK)
        countdown = time.time()
        while time.time() - countdown < 10:
            time.sleep(1)
            read = os.read(fifo, 1)
            if len(read) == 1:
                msg = msg + read
                countdown = time.time()
            if len(msg) == msg_len:
                send_msg(msg)
                os.unlink(fifo_path)
                sys.exit()
        print("Timeout")
        os.unlink(fifo_path)
        sys.exit()
    
    
    def second(msg):
        try:
            fifo = os.open(fifo_path, os.O_WRONLY | os.O_NONBLOCK)
            os.write(fifo, msg)
            os.close(fifo)
        except OSError as err:
            print("Could not open fifo", err)
            sys.exit(1)
    
        sys.exit()
    
    
    def send_msg(msg):
        print(msg)
        try:
            s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
            s.connect((args.ip, args.port))
        except Exception as err:
            print("Could not connect to gateway:", err)
            sys.exit(1)
    
        rnd_len = int((16-msg_len)/2)
        msg = Random.new().read(rnd_len) + msg + Random.new().read(rnd_len)
        cipher = AES.new(args.key, AES.MODE_ECB)
        msg = base64.b64encode(cipher.encrypt(msg))
        print(msg)
        time.sleep(1)
        s.sendall(str.encode(str(args.node)) + b';1;1;0;47;' + msg + b'\n')
        s.shutdown(socket.SHUT_WR)
        s.close()
    
    
    def parse_arguments():
        parser = argparse.ArgumentParser(description='Encode message')
        parser.add_argument('--node', dest='node',
                            help='Destination node', type=int, required=True)
        parser.add_argument('--msg', dest='msg', help='Message to encode and send',
                            type=str, required=True)
        parser.add_argument('--ip', dest='ip', help='IP of the GW',
                            type=str, default="127.0.0.1")
        parser.add_argument('--port', dest='port',
                            help='Port of the GW', type=int, default=5003)
        parser.add_argument('--key', dest='key',
                            help='AES key. 16 bit length', type=str, default=KEY)
        result = parser.parse_args()
        return result
    
    
    if __name__ == '__main__':
        args = parse_arguments()
        if len(args.key) != 16:
            print("Key must be 16 bit long")
            sys.exit()
        if len(args.msg) == 1:
            path = sys.executable + '\\ ' + __file__
            pid = check_output(['pgrep', '-f', path])
            pid = pid.decode('ascii')
            pid = pid.replace(str(os.getpid()), '')
            if len(pid.split()) == 0:
                print("First")
                if os.fork() > 0:
                    sys.exit()
                first(str.encode(args.msg))
            else:
                print("Second")
                second(str.encode(args.msg))
    
        elif len(args.msg) == msg_len:
            send_msg(str.encode(args.msg))
        else:
            print("Message should be %d bytes long." % msg_len)
            sys.exit()
    

    Arduino code for a simple test node:

    #define MY_DEBUG
    
    #define MY_RADIO_NRF24
    
    #include <MySensors.h>
    #include <AES.h>
    #include <rBase64.h>
    
    #define CHILD_ID 1
    
    char *AESkey = "abcdefGHIJKLmnop";
    char key[] = "1234";
    
    AES aes;  //Initialize AES library
    
    MyMessage msg(CHILD_ID, V_LOCK_STATUS);
    
    void before ()
    {
      
    }
    void setup()
    {
      aes.set_key(AESkey, 16);  //Set AES key
    }
    
    void presentation()
    {
      // Send the sketch version information to the gateway and Controller
      sendSketchInfo("AES test node", "1.0");
    
    }
    
    void loop()
    {
      
    }
    
    void receive(const MyMessage &message) {
      if (message.type == V_TEXT) {
        if (check(message.getString())) {
          Serial.println("Key is correct!")
        } else {
          Serial.println("Key is wrong!")
        }
      }
    }
    
    bool check(char* message)
    {
      char plain[16];
      rbase64.decode(message);
      aes.decrypt(rbase64.result(), plain);
      for (int i = 0; i < 4; i++) {  //Check decoded key
        if (plain[i + 6] != key[i]) {
          return false;
          break;
        }
      }
      return true;
    }
    

    For this code to work you will need python-crypto (or python3-crypto) and prgep packages to be installed in your system. For arduino part install these libraries: https://github.com/spaniakos/AES, https://github.com/boseji/rBASE64. And don't forget to make python script executable.

    Feel free to correct any mistakes or suggest any ideas how to improve this.


 

482
Online

7.9k
Users

8.8k
Topics

94.3k
Posts