Project: The Private Security Raspberry Pi Cam

When it comes to security cameras, there is a dearth of open source platforms that are hardware agnostic & can vouch for an end-to-end security from the point an image is captured by the camera till the point it travel through a communication channel to get uploaded to the cloud where only a set of authorized users can view or process it.

That's where the "The Private Security Camera" comes in. It will be a lightweight solution, which can run even on a Raspberry Pi with an attached camera. It will be ideal for situations where we want the camera to only capture an image when it detects a motion(instead of uploading 1000hrs of video feed) and notify its authorized user via a secure communication channel about its discovery.

Technical goal

The goal of this project is to develop software for a camera that can safely upload onto the cloud encrypted images that the Raspberry pi camera captures when it detects a motion in front. The cloud in this case would be our own secure "Matrix" home server, which hosts a chat room whose members are our authorized humans or ML bots.

Technical ingredients:

Why MATRIX?

Matrix is a relatively new communication/messaging system built on the principles of GIT. In essence there'd be no single server which will host and store all messages. Instead every client involved in the chat can have their own server also called as the HomeServer. And everyone's Homeserver involved in that chat will have a Backup of those messages. You can find out more about it on: www.matrix.org

In order to implement Matrix on the server we'd use its official server implementation called as Synapse.

Setting up Google Cloud VM:

Although I won't go into too much details about GCVM, but will give some top level parameters that I set and the librariess I installed during the setup.

Setting up synapse on the Linux Homeserver:

Also, my guide on setting up Synapse is based off on the following other wonderful guides:
https://upcloud.com/community/tutorials/install-matrix-synapse/
https://github.com/matrix-org/synapse/blob/master/INSTALL.md
https://www.natrius.eu/dokuwiki/doku.php?id=digital:server:matrixsynapse

Setting up Motion library on RaspberryPi:

Now that our HomeServer is all set and we have registered a user. Now is the time to make our raspberry pi with a cam detect motion.
That's where "MOTION PROJECT" comes in, it's an open source library and quite popular. I believe I installed it on my Pi as a package rather than from source. Command used was:

> sudo apt-get install motion

Where is the conf file?

On raspberryPi Motion has motion.conf in etc/motion. > Turn off stream_localhost, in order to view the stream externally using ip:8081 in a web browser.

How to start motion?

> Command line: sudo motion -n //Where -n is to run it in non demon mode. > To stop it: Ctrl^c Important configurations to look for when detecting motion: Width: 320 -> 640 Height: 240 -> 480 framerate: Default =2; Maximum number of frames to be captured from the camera per second. I changed to 24 lightswitch_percent : To set in order to avoid FALSE motion detection when light is turned on in a room. minimum_motion_frames: How many frames should change in a row for Motion to set it as "MotionDetection". Relevant resources: https://motion-project.github.io/motion_guide.html https://unix.stackexchange.com/questions/401287/motion-is-taking-an-image-every-second-even-though-it-is-setup-not-to https://www.unix.com/60599-post3.html https://www.hackster.io/robin-cole/motion-activated-image-capture-and-classification-of-birds-6ef1ce https://www.home-assistant.io/

Setting up your Matrix nio client along with Encryption:

This I felt was one of the most chalenging parts in making an end-end communication system between the Pi and Riot client.
Its a pretty advanced and capable client side library for Matrix. We'd also want our messages to be Encrypted before they go out of Pi to the Server and Chat room on Riot. Since OLM library's installion is a prerequisite for matrix-nio[e2e]. And installing OLM on a Mac or Linux is very tricky, but I was able to figure out some ways to install it.

How does Encryption work in general in Matrix and Nio?
Matrix uses Double Rachet in Matrix. What is double rachet: https://signal.org/docs/specifications/doubleratchet/
Natrix encrypt messages to devices rather than to users. e.g. if the device gets stolen then the device can be blacklisted and whoever has the stolen device wouldn't be able to see new messages.
How many keys are involved?
Device keys : Every device has a unique decryption keys. That key is not same for a user. A user will have as many decrypt keys as the number of devices he/she have.
1) Decryption keys: A client composes a message for a destination chat room
2) A new random key is generated for that message.
3) Encrypted message sent to server.
4) All devices associated with the chat room are sent "THE" decryption key encrypted with a device's encryption key.
5) The device will decrypt the "message decryption key" using the unique decryption key provided to each device.
6) Finally when the encrypted message arrives device will use the message decryption key to decrypt that message.

Note that when you log out and log in again, your new session is considered a new device from Riot's perspective.

How to view all your sessions and devices being used + deviceIDS:
> Login to Riot website.
> Profile setting -> security -> You can see all the sessions and info.

Installing OLM on MAC:


Have brew installed on your Mac.
brew install libolm


Installing OLM on Linux/Raspberry Pi:


        
If the warning says: libfii not installed during "make olm-python3", use: 
1) sudo apt-get install libffi6 libffi-dev 2) git clone https://gitlab.matrix.org/matrix-org/olm.git 3) cd olm 4) make 5) sudo make install 6) sudo ldconfig 7) cd python 8) sudo python3 setup.py install

Installing matrix-nio[e2e]

Go to your project and install matrix-nio[e2e] using: pip install matrix-nio[e2e]
Now that the Matrix-nio[e2e] is installed, we can write the actual code that encrypts our Text/Images.
Few of the functionalities we'd want in our Pi client code are:
Let's name the client user as "pi" from here on.
1) When you log in for the first time as Pi, you receive a device_id and access_token, we want to store it on disk.
Because on every fresh login of the same user Pi you receive a new device id. And on Riot client on the web, we have to authenticate every session of the user Pi. So it's better we authenticate the RaspberryPi session just once from Riot client by going into the chat room settings.
2) Fetch the stored device_id and access_token from the disk, before sending messages.
3) Trusting all the device ids(remember a user can have multiple device ids) that are present in the chat room and those which should be able to download the message or images.
4) Uploading the image and getting a link, which is then sent as a message to the chat room.

The complete code is available here which implements all the above functionalities.
Still let me give a code snippet of how to send a text message and an image as a message.

Text message:


    
import asyncio
from nio import AsyncClient

async def main():
    client = AsyncClient("https://camera.anantserver.org", "@pi:camera.anantserver.org")
    a = await client.login("password")
    asyncio.run(client.sync_forever(30000))
    b = await client.room_send(
        room_id="!pmoszSetEtIHfZScMY:camera.anantserver.org",
        message_type="m.room.message",
        content={
            "msgtype": "m.text",
            "body": "Hello World"
        }
    )
    print(b)
    await client.close()

asyncio.get_event_loop().run_until_complete(main())
    
    

Image as a message(NOTE the message_type):

        
import asyncio
from nio import AsyncClient, Api
from nio.api import ResizingMethod
import os



async def main():
    client = AsyncClient("https://camera.anantserver.org", "@anant:camera.anantserver.org")
    a = await client.login("password")
    asyncio.run(client.sync_forever(30000))
    
    c = Api.upload(token, "Morty_Smith.jpg")
    path = os.path.dirname(os.path.abspath(__file__))
    path = os.path.join(path, "Morty_Smith.jpg")
    a, n = await client.upload(
        lambda *_ : path, "image/jpg", "Morty_Smith.jpg"
    )
    print(a.content_uri)
    #### Uploading thumbnail wasn't a success, still puting in the code if someone wanna extend from here.
    thumbObj = await client.thumbnail("https://camera.anantserver.org",a.content_uri,width=500,height=500, method=ResizingMethod.crop)
    tubmb_uri = thumbObj.transport_response.real_url
    tubmb_uri = str(tubmb_uri)
    
    b = await client.room_send(
        room_id="!pmoszSetEtIHfZScMY:camera.anantserver.org",
        message_type="m.room.message",
        content={
            "msgtype": "m.image",
            "body": "Morty_Smith.jpg",
            "url" : a.content_uri
        }
    )
    print(b)
    await client.close()

asyncio.get_event_loop().run_until_complete(main())

               
        
        

THE LAST STEP


Now when you have started the Motion library using sudo motion -n, whenever it will detect some motion it will save one of the pictures in the directory specified by you in the config file in "etc/motion". We have to device a way such that whenever a new image gets saved in the folder, it be sent to the matrix nio client on the Pi.

Here comes the "WATCHDOG" python package, which monitors any directory for any change events like addition, modification, deletion.
The idea is, as soon as a new image comes in, WATCHDOG code will send the image as a command line argument to the nioClient code, which we created in the last section. Then it will also delete the image to clear space in the Pi. The code for WATCHDOG is:

            
import watchdog.events
import watchdog.observers
import time
import os


class Handler(watchdog.events.PatternMatchingEventHandler):
    def __init__(self):
        # Set the patterns for PatternMatchingEventHandler
        watchdog.events.PatternMatchingEventHandler.__init__(self, patterns=['*.jpg'],
                                                                ignore_directories=True, case_sensitive=False)

    def on_created(self, event):
        print("Watchdog received created event - % s." % event.src_path)
        os.system('python3 EncryptionTestAdvanced.py {}'.format(event.src_path))
        print("returned from the Fn and uploaded the image and MIGHT delete it.")
        os.system("rm -f {}".format(event.src_path))

        # Event is created, you can process it now

    # def on_modified(self, event):
    #     print("Watchdog received modified event - % s." % event.src_path)
    #     # Event is modified, you can process it now

    # def on_any_event(self, event):
    #     if event.is_directory:
    #         return None
    #
    #     elif event.event_type == 'created':
    #         # Event is created, you can process it now
    #         print("Watchdog received created event - % s." % event.src_path)
    #     elif event.event_type == 'modified':
    #         # Event is modified, you can process it now
    #         print("Watchdog received modified event - % s." % event.src_path)


if __name__ == "__main__":
    src_path = r"/Users/anantpathak/Learning/ProjectSecretCam/DummyFolder"
    event_handler = Handler()
    observer = watchdog.observers.Observer()
    observer.schedule(event_handler, path=src_path, recursive=True)
    observer.start()
    try:
        while True:
            time.sleep(1)
    except KeyboardInterrupt:
        observer.stop()
    observer.join()      
            
        

What's next?

I have some interesting ideas I'd love to implement in the near future, some of them are:
If you wish to ask me something or loved this article feel free to send me an email on : anant.p.pathak@outlook.com