mrlt8 / docker-wyze-bridge

WebRTC/RTSP/RTMP/LL-HLS bridge for Wyze cams in a docker container
GNU Affero General Public License v3.0
2.6k stars 160 forks source link

[FR] Option to encrypt pickle/state file #887

Open jslay88 opened 1 year ago

jslay88 commented 1 year ago

Just to add a little more security, since Wyze accounts can have things tied to them such as front door locks, it would be nice to be able to supply an encryption key for the application to use (either via UTF-8 binary file/single-line text file (same thing), or envvar) to encrypt and decrypt the pickle file. This way, the pickle file will be encrypted at rest.

I'd recommend going the easy route and just using Fernet.

import os
import pickle

from cryptography.fernet import Fernet

'''
Generate a key using Fernet.generate_key(), then store on environment variable or file.
Such as 

os.environ['ENCRYPTION_KEY'] = Fernet.generate_key().decode()

or

with open('my_secret.key', 'wb') as f:
    f.write(Fernet.generate_key())
os.environ['ENCRYPTION_KEY_PATH'] = 'my_secret.key'
'''

def load_key(fp: str = None) -> bytes | None:
    if fp:
        with open(fp, 'rb') as f:
            return f.read()
    if key := os.getenv('ENCRYPTION_KEY'):
        return key.encode()

def load_pickle(fp: str) -> object:
    if key := load_key(os.getenv('ENCRPYTION_KEY_PATH')):
        fernet = Fernet(key)
        with open(fp, 'rb') as f:
            return pickle.loads(fernet.decrypt(f.read()))
    with open(fp, 'rb') as f:
        return pickle.load(f)

def save_pickle(obj: object, fp: str):
    if key := load_key(os.getenv('ENCRPYTION_KEY_PATH')):
        fernet = Fernet(key)
        with open(fp, 'wb') as f:
            f.write(fernet.encrypt(pickle.dumps(obj)))
        return
    with open(fp, 'wb') as f:
        pickle.dump(obj, f)

Then adding something to the GUI where you can encrpyt, and it returns the key for safe storage. Further implementation best as you see fit.

You may also want to abandon pickle at some point, as there are inherit security risks to using it, as well as other issues when upgrading supporting Python modules and Python version. Usually, pickle is reserved for when you have no better serialization method (such as JSON or others). See also.

From a quick look at your code (will admit, looked for about 5 minutes), it looks like you may be able to use a dataclass to store all of your state, then you could easily serialize to JSON with json.dumps(my_state.__dict__), encode it and feed it to Fernet encrypt. You could also then unserialize using dict spreader/expander or the dacite module for nested structures.

import json
from dataclasses import dataclass

@dataclass
class MyState:
    a: str
    b: bool
    c: float

    def some_method(self) -> bool:
        print(self.a)
        if self.b:
            return self.b
        return self.c > 10

state = MyState(a='Hello, World!', b=False, c=20)
state.some_method()
data = json.dumps(state.__dict__)
fernet = Fernet(Fernet.generate_key())
encrypted = fernet.encrypt(data.encode())
decrypted = fernet.decrypt(encrypted).decode()
data == decrypted

new_state = MyState(**json.loads(decrypted))
new_state.some_method()
state == new_state

Thankfully, this Fernet implementation is agnostic to whatever serialization method you use, as you just feed it bytes to encrypt and decrypt (whether that's JSON, pickle, etc.).

mrlt8 commented 1 year ago

Thanks! Will look into Fernet. I've been wanting to move away from pickle for a long while but haven't had a chance yet. Most of the pickled data is already using pydantic, so I think we could easily dump/load json.

jslay88 commented 1 year ago

Ahh yeah, didn't think about that. Pydantic has a JSON serialization method built in to the models.

mrlt8 commented 1 year ago

Any thoughts on requests-cache? It seems to offer a Fernet serialization option.

Could potentially offer a good balance of caching the necessary api responses while also being able to refresh the data every now and then.

jslay88 commented 1 year ago

Sorry for the delayed response. Busy week.

I have never actually used their serialization before. I have always just used it as a lightweight caching layer on requests, and never serialized to disk from it. However, I assume they are using very similar approaches, by just taking the byte stream from the response and encrypting it directly. But I assume you could have it handle whatever you want (say JSON), since it looks like you are going to have to implement your own SerializerPipeline to make it use Fernet for the dumps and loads stage of the SerializerPipeline regardless.

Also, was thinking about your implementation with Pydantic, wonder if it might be worth moving from Flask to FastAPI for the backend as well, giving you the ability to do async properly. FastAPI has direct support for Pydantic models as well (Flask may have this now, its been a minute since I've explored with it). But this would require you to either separate the frontend with node or similar, or integrate Jinja into FastAPI. Just ideas for you to play with.

I am just glad I didn't have to reverse engineer all this Wyze stuff with a proxy myself.