Skip to content

Commit

Permalink
implement reading and writing encrypted archives (#109)
Browse files Browse the repository at this point in the history
Co-authored-by: Jesse <[email protected]>
  • Loading branch information
Changaco and boytm authored May 25, 2021
1 parent 373b9b1 commit 8b37a36
Show file tree
Hide file tree
Showing 4 changed files with 75 additions and 25 deletions.
15 changes: 13 additions & 2 deletions libarchive/ffi.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,9 @@ def get_write_filter_function(filter_name):

# FFI declarations

# library version
version_number = ffi('version_number', [], c_int, check_int)

# archive_util

errno = ffi('errno', [c_archive_p], c_int)
Expand Down Expand Up @@ -317,5 +320,13 @@ def get_write_filter_function(filter_name):
ffi('write_close', [c_archive_p], c_int, check_int)
ffi('write_free', [c_archive_p], c_int, check_int)

# library version
ffi('version_number', [], c_int, check_int)
# archive encryption

try:
ffi('read_add_passphrase', [c_archive_p, c_char_p], c_int, check_int)
ffi('write_set_passphrase', [c_archive_p, c_char_p], c_int, check_int)
except AttributeError:
logger.info(
f"the libarchive being used (version {version_number()}, "
f"path {ffi.libarchive_path}) doesn't support encryption"
)
38 changes: 25 additions & 13 deletions libarchive/read.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,23 @@ def __iter__(self):


@contextmanager
def new_archive_read(format_name='all', filter_name='all'):
def new_archive_read(format_name='all', filter_name='all', passphrase=None):
"""Creates an archive struct suitable for reading from an archive.
Returns a pointer if successful. Raises ArchiveError on error.
"""
archive_p = ffi.read_new()
try:
if passphrase:
if not isinstance(passphrase, bytes):
passphrase = passphrase.encode('utf-8')
try:
ffi.read_add_passphrase(archive_p, passphrase)
except AttributeError:
raise NotImplementedError(
f"the libarchive being used (version {ffi.version_number()}, "
f"path {ffi.libarchive_path}) doesn't support encryption"
)
ffi.get_read_filter_function(filter_name)(archive_p)
ffi.get_read_format_function(format_name)(archive_p)
yield archive_p
Expand All @@ -46,23 +56,24 @@ def new_archive_read(format_name='all', filter_name='all'):
def custom_reader(
read_func, format_name='all', filter_name='all',
open_func=VOID_CB, close_func=VOID_CB, block_size=page_size,
archive_read_class=ArchiveRead
archive_read_class=ArchiveRead, passphrase=None,
):
"""Read an archive using a custom function.
"""
open_cb = OPEN_CALLBACK(open_func)
read_cb = READ_CALLBACK(read_func)
close_cb = CLOSE_CALLBACK(close_func)
with new_archive_read(format_name, filter_name) as archive_p:
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
ffi.read_open(archive_p, None, open_cb, read_cb, close_cb)
yield archive_read_class(archive_p)


@contextmanager
def fd_reader(fd, format_name='all', filter_name='all', block_size=4096):
def fd_reader(fd, format_name='all', filter_name='all', block_size=4096,
passphrase=None):
"""Read an archive from a file descriptor.
"""
with new_archive_read(format_name, filter_name) as archive_p:
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
try:
block_size = fstat(fd).st_blksize
except (OSError, AttributeError): # pragma: no cover
Expand All @@ -72,10 +83,11 @@ def fd_reader(fd, format_name='all', filter_name='all', block_size=4096):


@contextmanager
def file_reader(path, format_name='all', filter_name='all', block_size=4096):
def file_reader(path, format_name='all', filter_name='all', block_size=4096,
passphrase=None):
"""Read an archive from a file.
"""
with new_archive_read(format_name, filter_name) as archive_p:
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
try:
block_size = stat(path).st_blksize
except (OSError, AttributeError): # pragma: no cover
Expand All @@ -85,17 +97,17 @@ def file_reader(path, format_name='all', filter_name='all', block_size=4096):


@contextmanager
def memory_reader(buf, format_name='all', filter_name='all'):
def memory_reader(buf, format_name='all', filter_name='all', passphrase=None):
"""Read an archive from memory.
"""
with new_archive_read(format_name, filter_name) as archive_p:
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
ffi.read_open_memory(archive_p, cast(buf, c_void_p), len(buf))
yield ArchiveRead(archive_p)


@contextmanager
def stream_reader(stream, format_name='all', filter_name='all',
block_size=page_size):
block_size=page_size, passphrase=None):
"""Read an archive from a stream.
The `stream` object must support the standard `readinto` method.
Expand All @@ -115,14 +127,14 @@ def read_func(archive_p, context, ptrptr):
open_cb = OPEN_CALLBACK(VOID_CB)
read_cb = READ_CALLBACK(read_func)
close_cb = CLOSE_CALLBACK(VOID_CB)
with new_archive_read(format_name, filter_name) as archive_p:
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
ffi.read_open(archive_p, None, open_cb, read_cb, close_cb)
yield ArchiveRead(archive_p)


@contextmanager
def seekable_stream_reader(stream, format_name='all', filter_name='all',
block_size=page_size):
block_size=page_size, passphrase=None):
"""Read an archive from a seekable stream.
The `stream` object must support the standard `readinto`, 'seek' and
Expand All @@ -149,7 +161,7 @@ def seek_func(archive_p, context, offset, whence):
read_cb = READ_CALLBACK(read_func)
seek_cb = SEEK_CALLBACK(seek_func)
close_cb = CLOSE_CALLBACK(VOID_CB)
with new_archive_read(format_name, filter_name) as archive_p:
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
ffi.read_set_seek_callback(archive_p, seek_cb)
ffi.read_open(archive_p, None, open_cb, read_cb, close_cb)
yield ArchiveRead(archive_p)
45 changes: 36 additions & 9 deletions libarchive/write.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from contextlib import contextmanager
from ctypes import byref, cast, c_char, c_size_t, c_void_p, POINTER
import warnings

from . import ffi
from .entry import ArchiveEntry, new_archive_entry
Expand Down Expand Up @@ -145,16 +146,38 @@ def add_file_from_memory(


@contextmanager
def new_archive_write(format_name, filter_name=None, options=''):
def new_archive_write(format_name,
filter_name=None,
options='',
passphrase=None):
archive_p = ffi.write_new()
try:
ffi.get_write_format_function(format_name)(archive_p)
if filter_name:
ffi.get_write_filter_function(filter_name)(archive_p)
if passphrase and 'encryption' not in options:
if format_name == 'zip':
warnings.warn(
"The default encryption scheme of zip archives is weak. "
"Use `options='encryption=$type'` to specify the encryption "
"type you want to use. The supported values are 'zipcrypt' "
"(the weak default), 'aes128' and 'aes256'."
)
options += ',encryption' if options else 'encryption'
if options:
if not isinstance(options, bytes):
options = options.encode('utf-8')
ffi.write_set_options(archive_p, options)
if passphrase:
if not isinstance(passphrase, bytes):
passphrase = passphrase.encode('utf-8')
try:
ffi.write_set_passphrase(archive_p, passphrase)
except AttributeError:
raise NotImplementedError(
f"the libarchive being used (version {ffi.version_number()}, "
f"path {ffi.libarchive_path}) doesn't support encryption"
)
yield archive_p
ffi.write_close(archive_p)
ffi.write_free(archive_p)
Expand All @@ -168,7 +191,7 @@ def new_archive_write(format_name, filter_name=None, options=''):
def custom_writer(
write_func, format_name, filter_name=None,
open_func=VOID_CB, close_func=VOID_CB, block_size=page_size,
archive_write_class=ArchiveWrite, options=''
archive_write_class=ArchiveWrite, options='', passphrase=None,
):

def write_cb_internal(archive_p, context, buffer_, length):
Expand All @@ -179,7 +202,8 @@ def write_cb_internal(archive_p, context, buffer_, length):
write_cb = WRITE_CALLBACK(write_cb_internal)
close_cb = CLOSE_CALLBACK(close_func)

with new_archive_write(format_name, filter_name, options) as archive_p:
with new_archive_write(format_name, filter_name, options,
passphrase) as archive_p:
ffi.write_set_bytes_in_last_block(archive_p, 1)
ffi.write_set_bytes_per_block(archive_p, block_size)
ffi.write_open(archive_p, None, open_cb, write_cb, close_cb)
Expand All @@ -189,29 +213,32 @@ def write_cb_internal(archive_p, context, buffer_, length):
@contextmanager
def fd_writer(
fd, format_name, filter_name=None,
archive_write_class=ArchiveWrite, options=''
archive_write_class=ArchiveWrite, options='', passphrase=None,
):
with new_archive_write(format_name, filter_name, options) as archive_p:
with new_archive_write(format_name, filter_name, options,
passphrase) as archive_p:
ffi.write_open_fd(archive_p, fd)
yield archive_write_class(archive_p)


@contextmanager
def file_writer(
filepath, format_name, filter_name=None,
archive_write_class=ArchiveWrite, options=''
archive_write_class=ArchiveWrite, options='', passphrase=None,
):
with new_archive_write(format_name, filter_name, options) as archive_p:
with new_archive_write(format_name, filter_name, options,
passphrase) as archive_p:
ffi.write_open_filename_w(archive_p, filepath)
yield archive_write_class(archive_p)


@contextmanager
def memory_writer(
buf, format_name, filter_name=None,
archive_write_class=ArchiveWrite, options=''
archive_write_class=ArchiveWrite, options='', passphrase=None,
):
with new_archive_write(format_name, filter_name, options) as archive_p:
with new_archive_write(format_name, filter_name, options,
passphrase) as archive_p:
used = byref(c_size_t())
buf_p = cast(buf, c_void_p)
ffi.write_open_memory(archive_p, buf_p, len(buf), used)
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,4 @@ universal = 1
[flake8]
exclude=.?*,env*/
ignore = E226,E731,W504
max-line-length = 80
max-line-length = 85

0 comments on commit 8b37a36

Please sign in to comment.