from dataclasses import dataclass from datetime import datetime import plistlib from pathlib import Path from base64 import b64decode from hashlib import sha1 from . import mbdb from .mbdb import _FileMode from random import randbytes from typing import Optional # RWX:RX:RX DEFAULT = _FileMode.S_IRUSR | _FileMode.S_IWUSR | _FileMode.S_IXUSR | _FileMode.S_IRGRP | _FileMode.S_IXGRP | _FileMode.S_IROTH | _FileMode.S_IXOTH @dataclass class BackupFile: path: str domain: str def to_record(self) -> mbdb.MbdbRecord: raise NotImplementedError() @dataclass class ConcreteFile(BackupFile): contents: bytes src_path: Optional[str] = None owner: int = 0 group: int = 0 inode: Optional[int] = None mode: _FileMode = DEFAULT hash: bytes = None size: int = None def read_contents(self) -> bytes: contents = self.contents if self.contents == None: with open(self.src_path, "rb") as in_file: contents = in_file.read() # prepopulate hash and size self.hash = sha1(contents).digest() self.size = len(contents) return contents def to_record(self) -> mbdb.MbdbRecord: if self.inode is None: self.inode = int.from_bytes(randbytes(8), "big") if self.hash == None or self.size == None: self.read_contents() return mbdb.MbdbRecord( domain=self.domain, filename=self.path, link="", hash=self.hash, key=b"", mode=self.mode | _FileMode.S_IFREG, #unknown2=0, #unknown3=0, inode=self.inode, user_id=self.owner, group_id=self.group, mtime=int(datetime.now().timestamp()), atime=int(datetime.now().timestamp()), ctime=int(datetime.now().timestamp()), size=self.size, flags=4, properties=[] ) @dataclass class Directory(BackupFile): owner: int = 0 group: int = 0 mode: _FileMode = DEFAULT def to_record(self) -> mbdb.MbdbRecord: return mbdb.MbdbRecord( domain=self.domain, filename=self.path, link="", hash=b"", key=b"", mode=self.mode | _FileMode.S_IFDIR, #unknown2=0, #unknown3=0, inode=0, # inode is not respected for directories user_id=self.owner, group_id=self.group, mtime=int(datetime.now().timestamp()), atime=int(datetime.now().timestamp()), ctime=int(datetime.now().timestamp()), size=0, flags=4, properties=[] ) @dataclass class SymbolicLink(BackupFile): target: str owner: int = 0 group: int = 0 inode: Optional[int] = None mode: _FileMode = DEFAULT def to_record(self) -> mbdb.MbdbRecord: if self.inode is None: self.inode = int.from_bytes(randbytes(8), "big") return mbdb.MbdbRecord( domain=self.domain, filename=self.path, link=self.target, hash=b"", key=b"", mode=self.mode | _FileMode.S_IFLNK, #unknown2=0, #unknown3=0, inode=self.inode, user_id=self.owner, group_id=self.group, mtime=int(datetime.now().timestamp()), atime=int(datetime.now().timestamp()), ctime=int(datetime.now().timestamp()), size=0, flags=4, properties=[] ) @dataclass class AppBundle: identifier: str path: str container_content_class: str version: str = 804 @dataclass class Backup: files: list[BackupFile] apps: list[AppBundle] def write_to_directory(self, directory: Path): for file in self.files: if isinstance(file, ConcreteFile): #print("Writing", file.path, "to", directory / sha1((file.domain + "-" + file.path).encode()).digest().hex()) with open(directory / sha1((file.domain + "-" + file.path).encode()).digest().hex(), "wb") as f: f.write(file.read_contents()) with open(directory / "Manifest.mbdb", "wb") as f: f.write(self.generate_manifest_db().to_bytes()) with open(directory / "Status.plist", "wb") as f: f.write(self.generate_status()) with open(directory / "Manifest.plist", "wb") as f: f.write(self.generate_manifest()) with open(directory / "Info.plist", "wb") as f: f.write(plistlib.dumps({})) def generate_manifest_db(self): # Manifest.mbdb records = [] for file in self.files: records.append(file.to_record()) return mbdb.Mbdb(records=records) def generate_status(self) -> bytes: # Status.plist return plistlib.dumps({ "BackupState": "new", "Date": datetime.fromisoformat("1970-01-01T00:00:00+00:00"), "IsFullBackup": False, "SnapshotState": "finished", "UUID": "00000000-0000-0000-0000-000000000000", "Version": "2.4" }) def generate_manifest(self) -> bytes: # Manifest.plist plist = { "BackupKeyBag": b64decode(""" VkVSUwAAAAQAAAAFVFlQRQAAAAQAAAABVVVJRAAAABDud41d1b9NBICR1BH9JfVtSE1D SwAAACgAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAV1JBUAAA AAQAAAAAU0FMVAAAABRY5Ne2bthGQ5rf4O3gikep1e6tZUlURVIAAAAEAAAnEFVVSUQA AAAQB7R8awiGR9aba1UuVahGPENMQVMAAAAEAAAAAVdSQVAAAAAEAAAAAktUWVAAAAAE AAAAAFdQS1kAAAAoN3kQAJloFg+ukEUY+v5P+dhc/Welw/oucsyS40UBh67ZHef5ZMk9 UVVVSUQAAAAQgd0cg0hSTgaxR3PVUbcEkUNMQVMAAAAEAAAAAldSQVAAAAAEAAAAAktU WVAAAAAEAAAAAFdQS1kAAAAoMiQTXx0SJlyrGJzdKZQ+SfL124w+2Tf/3d1R2i9yNj9z ZCHNJhnorVVVSUQAAAAQf7JFQiBOS12JDD7qwKNTSkNMQVMAAAAEAAAAA1dSQVAAAAAE AAAAAktUWVAAAAAEAAAAAFdQS1kAAAAoSEelorROJA46ZUdwDHhMKiRguQyqHukotrxh jIfqiZ5ESBXX9txi51VVSUQAAAAQfF0G/837QLq01xH9+66vx0NMQVMAAAAEAAAABFdS QVAAAAAEAAAAAktUWVAAAAAEAAAAAFdQS1kAAAAol0BvFhd5bu4Hr75XqzNf4g0fMqZA ie6OxI+x/pgm6Y95XW17N+ZIDVVVSUQAAAAQimkT2dp1QeadMu1KhJKNTUNMQVMAAAAE AAAABVdSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAo2N2DZarQ6GPoWRgTiy/t djKArOqTaH0tPSG9KLbIjGTOcLodhx23xFVVSUQAAAAQQV37JVZHQFiKpoNiGmT6+ENM QVMAAAAEAAAABldSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAofe2QSvDC2cV7 Etk4fSBbgqDx5ne/z1VHwmJ6NdVrTyWi80Sy869DM1VVSUQAAAAQFzkdH+VgSOmTj3yE cfWmMUNMQVMAAAAEAAAAB1dSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kAAAAo7kLY PQ/DnHBERGpaz37eyntIX/XzovsS0mpHW3SoHvrb9RBgOB+WblVVSUQAAAAQEBpgKOz9 Tni8F9kmSXd0sENMQVMAAAAEAAAACFdSQVAAAAAEAAAAA0tUWVAAAAAEAAAAAFdQS1kA AAAo5mxVoyNFgPMzphYhm1VG8Fhsin/xX+r6mCd9gByF5SxeolAIT/ICF1VVSUQAAAAQ rfKB2uPSQtWh82yx6w4BoUNMQVMAAAAEAAAACVdSQVAAAAAEAAAAA0tUWVAAAAAEAAAA AFdQS1kAAAAo5iayZBwcRa1c1MMx7vh6lOYux3oDI/bdxFCW1WHCQR/Ub1MOv+QaYFVV SUQAAAAQiLXvK3qvQza/mea5inss/0NMQVMAAAAEAAAACldSQVAAAAAEAAAAA0tUWVAA AAAEAAAAAFdQS1kAAAAoD2wHX7KriEe1E31z7SQ7/+AVymcpARMYnQgegtZD0Mq2U55u xwNr2FVVSUQAAAAQ/Q9feZxLS++qSe/a4emRRENMQVMAAAAEAAAAC1dSQVAAAAAEAAAA A0tUWVAAAAAEAAAAAFdQS1kAAAAocYda2jyYzzSKggRPw/qgh6QPESlkZedgDUKpTr4Z Z8FDgd7YoALY1g=="""), "Lockdown": {}, "SystemDomainsVersion": "20.0", "Version": "9.1" } # add the apps if len(self.apps) > 0: plist["Applications"] = {} for app in self.apps: appInfo = { "CFBundleIdentifier": app.identifier, "CFBundleVersion": app.version, "ContainerContentClass": app.container_content_class, "Path": app.path } plist["Applications"][app.identifier] = appInfo return plistlib.dumps(plist)