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:
- Google cloud VM to host Matrix homeserver.
- Raspberry Pi with a Pi Camera to detect motion
- Custom Matrix based client on the Pi to send images as Encrypted messages onto the HomeServer.
- Off the shelf Matrix based client like Riot to receive messages from RaspberryPi.
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.
- Go to https://console.cloud.google.com
- Create your free/trial account, I used my student account. On the top left'd be a Hamburger menu -> Compute Engine -> VM Instances -> Create Instances.
- Give any name -> Your nearest regon for low latency -> MachineType: g1-small (1 vCPU, 1.7 GB memory); Series: N1 -> BootDisk: Debian Linux 10 -> Allow Http/Https Traffic -> Done.
- Right next to your VM Instance would be "SSH" button, click there and it'd open up your Linux via SSH on a terminal window. Not as fancy as a MAC/Windows but learn to live with it.
- sudo apt update
- sudo apt upgrade
- sudo reboot
- sudo apt install build-essential gdb python3-pycryptodome python3-pytest
- sudo apt install gcc-multilib libc6-dev-i386
- sudo apt install python3-pip
- sudo apt-get install build-essential python3-dev libffi-dev \
python3-pip python3-setuptools sqlite3 \
libssl-dev virtualenv libjpeg-dev libxslt1-dev
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
-
mkdir synapse cd synapse python3 -m venv .venv source .venv/bin/activate python3 -m pip install --upgrade pip python3 -m pip install --upgrade setuptools python3 -m pip install matrix-synapse
- How to upgrade the Matrix-Synapse installation at a later stage?
python3 -m pip install --upgrade matrix-synapse
- Let's create a configuration file, also called as the "homeserver.yaml" in your ~/synapse directory, which you created in the previous step.
cd ~/synapse python -m synapse.app.homeserver \ --server-name my.domain.name \ --config-path homeserver.yaml \ --generate-config \ --report-stats=[yes|no] E.g. python -m synapse.app.homeserver \ --server-name camera.anantserver.org \ --config-path homeserver.yaml \ --generate-config \ --report-stats=yes
-
In order to make your homeserver: https://camera.anantserver.org https enabled you'd need to generate a TLS certificate. Since I was on Debian I used a tool called as Certbot Client which helped me in that, for other Linux versions look here
sudo apt-get install certbot python-certbot-nginx sudo certbot certonly --nginx -d camera.anantserver.org
IMPORTANT NOTES: - Congratulations! Your certificate and chain have been saved at: /etc/letsencrypt/live/camera.anantserver.org/fullchain.pem Your key file has been saved at: /etc/letsencrypt/live/camera.anantserver.org/privkey.pem Your cert will expire on 2020-07-07. To obtain a new or tweaked version of this certificate in the future, simply run certbot again. To non-interactively renew *all* of your certificates, run "certbot renew" - Your account credentials have been saved in your Certbot configuration directory at /etc/letsencrypt. You should make a secure backup of this folder now. This configuration directory will also contain certificates and private keys obtained by Certbot so making regular backups of this folder is ideal. - If you like Certbot, please consider supporting our work by: Donating to ISRG / Let's Encrypt: https://letsencrypt.org/donate Donating to EFF: https://eff.org/donate-le
To test if certbot renew works and removing the dry run part would also renew the certificate:sudo certbot renew --dry-run
- At this point you can check your Homeserver to be live and running, type in the browser: https://camera.anantserver.org
- Now is the time to register the first person on Earth as a user of your Homeserver. You can make that user an admin when prompted.
sudo register_new_matrix_user -c homeserver.yaml http://localhost:8008
- You can go to Riot.im and enter the url of your homeserver and username, password of the user you just created.
Up till this point whatever we have done can be used to communicate securely among our friends using the Riot client and our homeserver. -
Databases in Matrix:
I feel its better to migrate to PostgreSql as your database than MySql which is the default one. When installing PostgreSql, I faced an error installing "psycopg2" which is a Python package required by Matrix for the code to talk to PSQL database.
I faced an error during the installation of PostgreSql. How I resolved it?
Install psycopg2: I faced an error while running the command: python3 -m pip install matrix-synapse[postgres] The error was due to it not being able to install psycopg2 in debian. TO resolve it, install libpq-dev , of which psycopg2 is a wrapper. sudo apt install libpq-dev python3-dev Then: python3 -m pip install psycopg2 Then again run the command: python3 -m pip install matrix-synapse[postgres] Then install: > sudo apt install postgresql > sudo -u postgres bash Create a username and password: > postgres@alice:/home/pathak/synapse$ > createuser --pwprompt synapse_user > Password: ****** Create your database, the name is Synapse. > CREATE DATABASE synapse ENCODING 'UTF8' LC_COLLATE='C' LC_CTYPE='C' template=template0 OWNER synapse_user; Open up the configuration file homeserver.yaml in ~/synapse: > Under database: name: psycopg2 args: user: "synapse_user" password: "*******" database: "synapse" host: "127.0.0.1" cp_min: 5 cp_max: 10
Databases:
How to fireup psql: > sudo -u postgres psql synapse synapse_user -W -h localhost Password: ******* Use psql commands like: > \dt for displaying all database tables > SELECT * FROM users; > SELECT * FROM rooms; // to view all rooms and their ids Deleting a user or a field: > DELETE FROM users WHERE name LIKE '@bob:%'; //If a user bob has already been created before. BUT if you delete the user this way, next time creating the same user would throw an error by Synapse.
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:- Using Machine learning algorithms like CNN on the server or on the edge device itself like the pi. Using Tensorflow's models we can use it to recognize an object of your interest, like an unwanted human or a bird. The TF can analyze the video on Pi and send us just the relevant image.
- When it comes to doing an end-end setup there are lot of installations, if I am to share this project to a large number of non tech savy consumers, I'd love to pack most of the extra setup in a package which can then be distributed using "sudo apt .."
- The current setup is geared toward having a RaspberryPi, hence having a device agnostic setup or one which covers many platforms would be awesome.