Privnote Data Decoder

I was feeling bored, so I decided to recreate the pyPrivnote library, which was made around 6 years ago and hasn’t received any updates since. Anyway, this is just a small portion of the code, and I haven’t yet decided if I want to post the full code because, you know, I’m feeling a bit lazy.

This code takes a Privnote URL, splits the URL by the # symbol since the string after the # serves as the key for encoding and decoding. Then, we take this key and perform some basic AES decryption to retrieve the raw paste data.

from base64 import b64decode
from hashlib import md5
from Crypto.Cipher import AES
from Crypto.Util.Padding import unpad
from dataclasses import dataclass, field
from tls_client import Session

@dataclass
class PrivnoteLink:
    _id: str = None
    _password: bytes = None
    _session: Session = field(default_factory=lambda: Session(client_identifier="firefox_115", random_tls_extension_order=True))
    
    @classmethod
    def from_link(cls, link):
        instance = cls()
        instance.link = link
        return instance

    @property
    def link(self):
        if self._id is not None and self._password is not None:
            return f"https://privnote.com/{self._id}#{self.decode_password()}"
        else:
            return None

    @link.setter
    def link(self, value):
        if value.startswith("privnote.com/"):
            value = "https://" + value
        value = value.split("https://privnote.com/")[1]
        value = value.split("#", maxsplit=1)
        self._id = value[0]
        if len(value) == 2 and value[1]:
            self._password = bytes(value[1], encoding="utf-8")

    def decode_password(self):
        if self._password is not None:
            return self._password.decode('utf-8')
        else:
            return None

    def read(self):
        headers = {"X-Requested-With": "XMLHttpRequest"}
        encrypted_data = self._session.delete(self.link, headers=headers).json()
        
        if not encrypted_data.get('data'):
            print(f'Note may have already been destroyed.')
            return 
        
        _data = b64decode(encrypted_data.get('data'))

        salt = _data[8:16]
        _data = _data[16:]

        pbe = self._openSSLkey(self._password, salt)
        key = pbe["key"]
        iv = pbe["iv"]

        cipher = AES.new(key, AES.MODE_CBC, iv)
        return unpad(cipher.decrypt(_data), AES.block_size).decode()

    @staticmethod
    def _openSSLkey(password_bytes, salt_arr):
        passalt = password_bytes + salt_arr
        result = cur_hash = md5(passalt).digest()
        for _ in range(2):
            cur_hash = md5(cur_hash + passalt).digest()
            result += cur_hash

        return {
            "key": result[0:4 * 8],
            "iv": result[4 * 8:4 * 8 + 16]
        }

    def __str__(self):
        return self.link

    def __repr__(self):
        return f"PrivnoteLink(_id={self._id}, _password={self.decode_password()})"

if __name__ == "__main__":
    privnote = PrivnoteLink.from_link("https://privnote.com/NWIpX2Sz#tzK9VUYir")
    decrypted_note = privnote.read()

    print(decrypted_note)