Skip to content

Commit 8b37a36

Browse files
Changacoboytm
andauthored
implement reading and writing encrypted archives (#109)
Co-authored-by: Jesse <boycht@gmail.com>
1 parent 373b9b1 commit 8b37a36

4 files changed

Lines changed: 75 additions & 25 deletions

File tree

libarchive/ffi.py

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,9 @@ def get_write_filter_function(filter_name):
154154

155155
# FFI declarations
156156

157+
# library version
158+
version_number = ffi('version_number', [], c_int, check_int)
159+
157160
# archive_util
158161

159162
errno = ffi('errno', [c_archive_p], c_int)
@@ -317,5 +320,13 @@ def get_write_filter_function(filter_name):
317320
ffi('write_close', [c_archive_p], c_int, check_int)
318321
ffi('write_free', [c_archive_p], c_int, check_int)
319322

320-
# library version
321-
ffi('version_number', [], c_int, check_int)
323+
# archive encryption
324+
325+
try:
326+
ffi('read_add_passphrase', [c_archive_p, c_char_p], c_int, check_int)
327+
ffi('write_set_passphrase', [c_archive_p, c_char_p], c_int, check_int)
328+
except AttributeError:
329+
logger.info(
330+
f"the libarchive being used (version {version_number()}, "
331+
f"path {ffi.libarchive_path}) doesn't support encryption"
332+
)

libarchive/read.py

Lines changed: 25 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,13 +28,23 @@ def __iter__(self):
2828

2929

3030
@contextmanager
31-
def new_archive_read(format_name='all', filter_name='all'):
31+
def new_archive_read(format_name='all', filter_name='all', passphrase=None):
3232
"""Creates an archive struct suitable for reading from an archive.
3333
3434
Returns a pointer if successful. Raises ArchiveError on error.
3535
"""
3636
archive_p = ffi.read_new()
3737
try:
38+
if passphrase:
39+
if not isinstance(passphrase, bytes):
40+
passphrase = passphrase.encode('utf-8')
41+
try:
42+
ffi.read_add_passphrase(archive_p, passphrase)
43+
except AttributeError:
44+
raise NotImplementedError(
45+
f"the libarchive being used (version {ffi.version_number()}, "
46+
f"path {ffi.libarchive_path}) doesn't support encryption"
47+
)
3848
ffi.get_read_filter_function(filter_name)(archive_p)
3949
ffi.get_read_format_function(format_name)(archive_p)
4050
yield archive_p
@@ -46,23 +56,24 @@ def new_archive_read(format_name='all', filter_name='all'):
4656
def custom_reader(
4757
read_func, format_name='all', filter_name='all',
4858
open_func=VOID_CB, close_func=VOID_CB, block_size=page_size,
49-
archive_read_class=ArchiveRead
59+
archive_read_class=ArchiveRead, passphrase=None,
5060
):
5161
"""Read an archive using a custom function.
5262
"""
5363
open_cb = OPEN_CALLBACK(open_func)
5464
read_cb = READ_CALLBACK(read_func)
5565
close_cb = CLOSE_CALLBACK(close_func)
56-
with new_archive_read(format_name, filter_name) as archive_p:
66+
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
5767
ffi.read_open(archive_p, None, open_cb, read_cb, close_cb)
5868
yield archive_read_class(archive_p)
5969

6070

6171
@contextmanager
62-
def fd_reader(fd, format_name='all', filter_name='all', block_size=4096):
72+
def fd_reader(fd, format_name='all', filter_name='all', block_size=4096,
73+
passphrase=None):
6374
"""Read an archive from a file descriptor.
6475
"""
65-
with new_archive_read(format_name, filter_name) as archive_p:
76+
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
6677
try:
6778
block_size = fstat(fd).st_blksize
6879
except (OSError, AttributeError): # pragma: no cover
@@ -72,10 +83,11 @@ def fd_reader(fd, format_name='all', filter_name='all', block_size=4096):
7283

7384

7485
@contextmanager
75-
def file_reader(path, format_name='all', filter_name='all', block_size=4096):
86+
def file_reader(path, format_name='all', filter_name='all', block_size=4096,
87+
passphrase=None):
7688
"""Read an archive from a file.
7789
"""
78-
with new_archive_read(format_name, filter_name) as archive_p:
90+
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
7991
try:
8092
block_size = stat(path).st_blksize
8193
except (OSError, AttributeError): # pragma: no cover
@@ -85,17 +97,17 @@ def file_reader(path, format_name='all', filter_name='all', block_size=4096):
8597

8698

8799
@contextmanager
88-
def memory_reader(buf, format_name='all', filter_name='all'):
100+
def memory_reader(buf, format_name='all', filter_name='all', passphrase=None):
89101
"""Read an archive from memory.
90102
"""
91-
with new_archive_read(format_name, filter_name) as archive_p:
103+
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
92104
ffi.read_open_memory(archive_p, cast(buf, c_void_p), len(buf))
93105
yield ArchiveRead(archive_p)
94106

95107

96108
@contextmanager
97109
def stream_reader(stream, format_name='all', filter_name='all',
98-
block_size=page_size):
110+
block_size=page_size, passphrase=None):
99111
"""Read an archive from a stream.
100112
101113
The `stream` object must support the standard `readinto` method.
@@ -115,14 +127,14 @@ def read_func(archive_p, context, ptrptr):
115127
open_cb = OPEN_CALLBACK(VOID_CB)
116128
read_cb = READ_CALLBACK(read_func)
117129
close_cb = CLOSE_CALLBACK(VOID_CB)
118-
with new_archive_read(format_name, filter_name) as archive_p:
130+
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
119131
ffi.read_open(archive_p, None, open_cb, read_cb, close_cb)
120132
yield ArchiveRead(archive_p)
121133

122134

123135
@contextmanager
124136
def seekable_stream_reader(stream, format_name='all', filter_name='all',
125-
block_size=page_size):
137+
block_size=page_size, passphrase=None):
126138
"""Read an archive from a seekable stream.
127139
128140
The `stream` object must support the standard `readinto`, 'seek' and
@@ -149,7 +161,7 @@ def seek_func(archive_p, context, offset, whence):
149161
read_cb = READ_CALLBACK(read_func)
150162
seek_cb = SEEK_CALLBACK(seek_func)
151163
close_cb = CLOSE_CALLBACK(VOID_CB)
152-
with new_archive_read(format_name, filter_name) as archive_p:
164+
with new_archive_read(format_name, filter_name, passphrase) as archive_p:
153165
ffi.read_set_seek_callback(archive_p, seek_cb)
154166
ffi.read_open(archive_p, None, open_cb, read_cb, close_cb)
155167
yield ArchiveRead(archive_p)

libarchive/write.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from contextlib import contextmanager
22
from ctypes import byref, cast, c_char, c_size_t, c_void_p, POINTER
3+
import warnings
34

45
from . import ffi
56
from .entry import ArchiveEntry, new_archive_entry
@@ -145,16 +146,38 @@ def add_file_from_memory(
145146

146147

147148
@contextmanager
148-
def new_archive_write(format_name, filter_name=None, options=''):
149+
def new_archive_write(format_name,
150+
filter_name=None,
151+
options='',
152+
passphrase=None):
149153
archive_p = ffi.write_new()
150154
try:
151155
ffi.get_write_format_function(format_name)(archive_p)
152156
if filter_name:
153157
ffi.get_write_filter_function(filter_name)(archive_p)
158+
if passphrase and 'encryption' not in options:
159+
if format_name == 'zip':
160+
warnings.warn(
161+
"The default encryption scheme of zip archives is weak. "
162+
"Use `options='encryption=$type'` to specify the encryption "
163+
"type you want to use. The supported values are 'zipcrypt' "
164+
"(the weak default), 'aes128' and 'aes256'."
165+
)
166+
options += ',encryption' if options else 'encryption'
154167
if options:
155168
if not isinstance(options, bytes):
156169
options = options.encode('utf-8')
157170
ffi.write_set_options(archive_p, options)
171+
if passphrase:
172+
if not isinstance(passphrase, bytes):
173+
passphrase = passphrase.encode('utf-8')
174+
try:
175+
ffi.write_set_passphrase(archive_p, passphrase)
176+
except AttributeError:
177+
raise NotImplementedError(
178+
f"the libarchive being used (version {ffi.version_number()}, "
179+
f"path {ffi.libarchive_path}) doesn't support encryption"
180+
)
158181
yield archive_p
159182
ffi.write_close(archive_p)
160183
ffi.write_free(archive_p)
@@ -168,7 +191,7 @@ def new_archive_write(format_name, filter_name=None, options=''):
168191
def custom_writer(
169192
write_func, format_name, filter_name=None,
170193
open_func=VOID_CB, close_func=VOID_CB, block_size=page_size,
171-
archive_write_class=ArchiveWrite, options=''
194+
archive_write_class=ArchiveWrite, options='', passphrase=None,
172195
):
173196

174197
def write_cb_internal(archive_p, context, buffer_, length):
@@ -179,7 +202,8 @@ def write_cb_internal(archive_p, context, buffer_, length):
179202
write_cb = WRITE_CALLBACK(write_cb_internal)
180203
close_cb = CLOSE_CALLBACK(close_func)
181204

182-
with new_archive_write(format_name, filter_name, options) as archive_p:
205+
with new_archive_write(format_name, filter_name, options,
206+
passphrase) as archive_p:
183207
ffi.write_set_bytes_in_last_block(archive_p, 1)
184208
ffi.write_set_bytes_per_block(archive_p, block_size)
185209
ffi.write_open(archive_p, None, open_cb, write_cb, close_cb)
@@ -189,29 +213,32 @@ def write_cb_internal(archive_p, context, buffer_, length):
189213
@contextmanager
190214
def fd_writer(
191215
fd, format_name, filter_name=None,
192-
archive_write_class=ArchiveWrite, options=''
216+
archive_write_class=ArchiveWrite, options='', passphrase=None,
193217
):
194-
with new_archive_write(format_name, filter_name, options) as archive_p:
218+
with new_archive_write(format_name, filter_name, options,
219+
passphrase) as archive_p:
195220
ffi.write_open_fd(archive_p, fd)
196221
yield archive_write_class(archive_p)
197222

198223

199224
@contextmanager
200225
def file_writer(
201226
filepath, format_name, filter_name=None,
202-
archive_write_class=ArchiveWrite, options=''
227+
archive_write_class=ArchiveWrite, options='', passphrase=None,
203228
):
204-
with new_archive_write(format_name, filter_name, options) as archive_p:
229+
with new_archive_write(format_name, filter_name, options,
230+
passphrase) as archive_p:
205231
ffi.write_open_filename_w(archive_p, filepath)
206232
yield archive_write_class(archive_p)
207233

208234

209235
@contextmanager
210236
def memory_writer(
211237
buf, format_name, filter_name=None,
212-
archive_write_class=ArchiveWrite, options=''
238+
archive_write_class=ArchiveWrite, options='', passphrase=None,
213239
):
214-
with new_archive_write(format_name, filter_name, options) as archive_p:
240+
with new_archive_write(format_name, filter_name, options,
241+
passphrase) as archive_p:
215242
used = byref(c_size_t())
216243
buf_p = cast(buf, c_void_p)
217244
ffi.write_open_memory(archive_p, buf_p, len(buf), used)

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,4 @@ universal = 1
44
[flake8]
55
exclude=.?*,env*/
66
ignore = E226,E731,W504
7-
max-line-length = 80
7+
max-line-length = 85

0 commit comments

Comments
 (0)