175 lines
5.6 KiB
Python
175 lines
5.6 KiB
Python
|
from Crypto.Cipher import AES
|
||
|
from hashlib import sha1
|
||
|
from struct import unpack
|
||
|
import os
|
||
|
|
||
|
MBDB_SIGNATURE = 'mbdb\x05\x00'
|
||
|
|
||
|
MASK_SYMBOLIC_LINK = 0xa000
|
||
|
MASK_REGULAR_FILE = 0x8000
|
||
|
MASK_DIRECTORY = 0x4000
|
||
|
|
||
|
def warn(msg):
|
||
|
print "WARNING: %s" % msg
|
||
|
|
||
|
class MBFileRecord(object):
|
||
|
def __init__(self, mbdb):
|
||
|
self.domain = self._decode_string(mbdb)
|
||
|
if self.domain is None:
|
||
|
warn("Domain name missing from record")
|
||
|
|
||
|
self.path = self._decode_string(mbdb)
|
||
|
if self.path is None:
|
||
|
warn("Relative path missing from record")
|
||
|
|
||
|
self.target= self._decode_string(mbdb) # for symbolic links
|
||
|
|
||
|
self.digest = self._decode_string(mbdb)
|
||
|
self.encryption_key = self._decode_data(mbdb)
|
||
|
|
||
|
data = mbdb.read(40) # metadata, fixed size
|
||
|
|
||
|
self.mode, = unpack('>H', data[0:2])
|
||
|
if not(self.is_regular_file() or self.is_symbolic_link() or self.is_directory()):
|
||
|
print self.mode
|
||
|
warn("File type mising from record mode")
|
||
|
|
||
|
if self.is_symbolic_link() and self.target is None:
|
||
|
warn("Target required for symblolic links")
|
||
|
|
||
|
self.inode_number = unpack('>Q', data[2:10])
|
||
|
self.user_id, = unpack('>I', data[10:14])
|
||
|
self.group_id = unpack('>I', data[14:18])
|
||
|
self.last_modification_time, = unpack('>i', data[18:22])
|
||
|
self.last_status_change_time, = unpack('>i', data[22:26])
|
||
|
self.birth_time, = unpack('>i', data[26:30])
|
||
|
self.size, = unpack('>q', data[30:38])
|
||
|
|
||
|
if self.size != 0 and not self.is_regular_file():
|
||
|
warn("Non-zero size for a record which is not a regular file")
|
||
|
|
||
|
self.protection_class = ord(data[38])
|
||
|
|
||
|
num_attributes = ord(data[39])
|
||
|
if num_attributes == 0:
|
||
|
self.extended_attributes = None
|
||
|
else:
|
||
|
self.extended_attributes = {}
|
||
|
for i in xrange(num_attributes):
|
||
|
k = self._decode_string(mbdb)
|
||
|
v = self._decode_data(mbdb)
|
||
|
self.extended_attributes[k] = v
|
||
|
|
||
|
def _decode_string(self, s):
|
||
|
s_len, = unpack('>H', s.read(2))
|
||
|
if s_len == 0xffff:
|
||
|
return None
|
||
|
return s.read(s_len)
|
||
|
|
||
|
def _decode_data(self, s):
|
||
|
return self._decode_string(s)
|
||
|
|
||
|
def type(self):
|
||
|
return self.mode & 0xf000
|
||
|
|
||
|
def is_symbolic_link(self):
|
||
|
return self.type() == MASK_SYMBOLIC_LINK
|
||
|
|
||
|
def is_regular_file(self):
|
||
|
return self.type() == MASK_REGULAR_FILE
|
||
|
|
||
|
def is_directory(self):
|
||
|
return self.type() == MASK_DIRECTORY
|
||
|
|
||
|
class MBDB(object):
|
||
|
def __init__(self, path):
|
||
|
self.files = {}
|
||
|
self.backup_path = path
|
||
|
self.keybag = None
|
||
|
# open the database
|
||
|
mbdb = file(path + '/Manifest.mbdb', 'rb')
|
||
|
|
||
|
# skip signature
|
||
|
signature = mbdb.read(len(MBDB_SIGNATURE))
|
||
|
if signature != MBDB_SIGNATURE:
|
||
|
raise Exception("Bad mbdb signature")
|
||
|
try:
|
||
|
while True:
|
||
|
rec = MBFileRecord(mbdb)
|
||
|
fn = rec.domain + "-" + rec.path
|
||
|
sb = sha1(fn).digest().encode('hex')
|
||
|
if len(sb) % 2 == 1:
|
||
|
sb = '0'+sb
|
||
|
self.files[sb] = rec
|
||
|
except:
|
||
|
mbdb.close()
|
||
|
|
||
|
def get_file_by_name(self, filename):
|
||
|
for (k, v) in self.files.iteritems():
|
||
|
if v.path == filename:
|
||
|
return (k, v)
|
||
|
return None
|
||
|
|
||
|
def extract_backup(self, output_path):
|
||
|
for record in self.files.values():
|
||
|
# create directories if they do not exist
|
||
|
# makedirs throw an exception, my code is ugly =)
|
||
|
if record.is_directory():
|
||
|
try:
|
||
|
os.makedirs(os.path.join(output_path, record.domain, record.path))
|
||
|
except:
|
||
|
pass
|
||
|
|
||
|
for (filename, record) in self.files.items():
|
||
|
# skip directories
|
||
|
if record.is_directory():
|
||
|
continue
|
||
|
self.extract_file(filename, record, output_path)
|
||
|
|
||
|
def extract_file(self, filename, record, output_path):
|
||
|
# adjust output file name
|
||
|
if record.is_symbolic_link():
|
||
|
out_file = record.target
|
||
|
else:
|
||
|
out_file = record.path
|
||
|
|
||
|
# read backup file
|
||
|
try:
|
||
|
f1 = file(os.path.join(self.backup_path, filename), 'rb')
|
||
|
except(IOError):
|
||
|
warn("File %s (%s) has not been found" % (filename, record.path))
|
||
|
return
|
||
|
|
||
|
# write output file
|
||
|
output_path = os.path.join(output_path, record.domain, out_file)
|
||
|
print("Writing %s" % output_path)
|
||
|
f2 = file(output_path, 'wb')
|
||
|
|
||
|
aes = None
|
||
|
|
||
|
if record.encryption_key is not None and self.keybag: # file is encrypted!
|
||
|
key = self.keybag.unwrapKeyForClass(record.protection_class, record.encryption_key[4:])
|
||
|
if not key:
|
||
|
warn("Cannot unwrap key")
|
||
|
return
|
||
|
aes = AES.new(key, AES.MODE_CBC, "\x00"*16)
|
||
|
|
||
|
while True:
|
||
|
data = f1.read(8192)
|
||
|
if not data:
|
||
|
break
|
||
|
if aes:
|
||
|
data2 = data = aes.decrypt(data)
|
||
|
f2.write(data)
|
||
|
|
||
|
f1.close()
|
||
|
if aes:
|
||
|
c = data2[-1]
|
||
|
i = ord(c)
|
||
|
if i < 17 and data2.endswith(c*i):
|
||
|
f2.truncate(f2.tell() - i)
|
||
|
else:
|
||
|
warn("Bad padding, last byte = 0x%x !" % i)
|
||
|
|
||
|
f2.close()
|