Skip to content

Commit 5c32af7

Browse files
bpo-38334: Fix seeking backward on an encrypted zipfile.ZipExtFile. (GH-16937)
Test by Daniel Hillier.
1 parent a8fb932 commit 5c32af7

File tree

3 files changed

+70
-26
lines changed

3 files changed

+70
-26
lines changed

Lib/test/test_zipfile.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1934,6 +1934,44 @@ def test_unicode_password(self):
19341934
self.assertRaises(TypeError, self.zip.open, "test.txt", pwd="python")
19351935
self.assertRaises(TypeError, self.zip.extract, "test.txt", pwd="python")
19361936

1937+
def test_seek_tell(self):
1938+
self.zip.setpassword(b"python")
1939+
txt = self.plain
1940+
test_word = b'encryption'
1941+
bloc = txt.find(test_word)
1942+
bloc_len = len(test_word)
1943+
with self.zip.open("test.txt", "r") as fp:
1944+
fp.seek(bloc, os.SEEK_SET)
1945+
self.assertEqual(fp.tell(), bloc)
1946+
fp.seek(-bloc, os.SEEK_CUR)
1947+
self.assertEqual(fp.tell(), 0)
1948+
fp.seek(bloc, os.SEEK_CUR)
1949+
self.assertEqual(fp.tell(), bloc)
1950+
self.assertEqual(fp.read(bloc_len), txt[bloc:bloc+bloc_len])
1951+
1952+
# Make sure that the second read after seeking back beyond
1953+
# _readbuffer returns the same content (ie. rewind to the start of
1954+
# the file to read forward to the required position).
1955+
old_read_size = fp.MIN_READ_SIZE
1956+
fp.MIN_READ_SIZE = 1
1957+
fp._readbuffer = b''
1958+
fp._offset = 0
1959+
fp.seek(0, os.SEEK_SET)
1960+
self.assertEqual(fp.tell(), 0)
1961+
fp.seek(bloc, os.SEEK_CUR)
1962+
self.assertEqual(fp.read(bloc_len), txt[bloc:bloc+bloc_len])
1963+
fp.MIN_READ_SIZE = old_read_size
1964+
1965+
fp.seek(0, os.SEEK_END)
1966+
self.assertEqual(fp.tell(), len(txt))
1967+
fp.seek(0, os.SEEK_SET)
1968+
self.assertEqual(fp.tell(), 0)
1969+
1970+
# Read the file completely to definitely call any eof integrity
1971+
# checks (crc) and make sure they still pass.
1972+
fp.read()
1973+
1974+
19371975
class AbstractTestsWithRandomBinaryFiles:
19381976
@classmethod
19391977
def setUpClass(cls):

Lib/zipfile.py

Lines changed: 31 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -792,10 +792,10 @@ class ZipExtFile(io.BufferedIOBase):
792792
# Chunk size to read during seek
793793
MAX_SEEK_READ = 1 << 24
794794

795-
def __init__(self, fileobj, mode, zipinfo, decrypter=None,
795+
def __init__(self, fileobj, mode, zipinfo, pwd=None,
796796
close_fileobj=False):
797797
self._fileobj = fileobj
798-
self._decrypter = decrypter
798+
self._pwd = pwd
799799
self._close_fileobj = close_fileobj
800800

801801
self._compress_type = zipinfo.compress_type
@@ -810,11 +810,6 @@ def __init__(self, fileobj, mode, zipinfo, decrypter=None,
810810

811811
self.newlines = None
812812

813-
# Adjust read size for encrypted files since the first 12 bytes
814-
# are for the encryption/password information.
815-
if self._decrypter is not None:
816-
self._compress_left -= 12
817-
818813
self.mode = mode
819814
self.name = zipinfo.filename
820815

@@ -835,6 +830,30 @@ def __init__(self, fileobj, mode, zipinfo, decrypter=None,
835830
except AttributeError:
836831
pass
837832

833+
self._decrypter = None
834+
if pwd:
835+
if zipinfo.flag_bits & 0x8:
836+
# compare against the file type from extended local headers
837+
check_byte = (zipinfo._raw_time >> 8) & 0xff
838+
else:
839+
# compare against the CRC otherwise
840+
check_byte = (zipinfo.CRC >> 24) & 0xff
841+
h = self._init_decrypter()
842+
if h != check_byte:
843+
raise RuntimeError("Bad password for file %r" % zipinfo.orig_filename)
844+
845+
846+
def _init_decrypter(self):
847+
self._decrypter = _ZipDecrypter(self._pwd)
848+
# The first 12 bytes in the cypher stream is an encryption header
849+
# used to strengthen the algorithm. The first 11 bytes are
850+
# completely random, while the 12th contains the MSB of the CRC,
851+
# or the MSB of the file time depending on the header type
852+
# and is used to check the correctness of the password.
853+
header = self._fileobj.read(12)
854+
self._compress_left -= 12
855+
return self._decrypter(header)[11]
856+
838857
def __repr__(self):
839858
result = ['<%s.%s' % (self.__class__.__module__,
840859
self.__class__.__qualname__)]
@@ -1061,6 +1080,8 @@ def seek(self, offset, whence=0):
10611080
self._decompressor = _get_decompressor(self._compress_type)
10621081
self._eof = False
10631082
read_offset = new_pos
1083+
if self._decrypter is not None:
1084+
self._init_decrypter()
10641085

10651086
while read_offset > 0:
10661087
read_len = min(self.MAX_SEEK_READ, read_offset)
@@ -1524,32 +1545,16 @@ def open(self, name, mode="r", pwd=None, *, force_zip64=False):
15241545

15251546
# check for encrypted flag & handle password
15261547
is_encrypted = zinfo.flag_bits & 0x1
1527-
zd = None
15281548
if is_encrypted:
15291549
if not pwd:
15301550
pwd = self.pwd
15311551
if not pwd:
15321552
raise RuntimeError("File %r is encrypted, password "
15331553
"required for extraction" % name)
1554+
else:
1555+
pwd = None
15341556

1535-
zd = _ZipDecrypter(pwd)
1536-
# The first 12 bytes in the cypher stream is an encryption header
1537-
# used to strengthen the algorithm. The first 11 bytes are
1538-
# completely random, while the 12th contains the MSB of the CRC,
1539-
# or the MSB of the file time depending on the header type
1540-
# and is used to check the correctness of the password.
1541-
header = zef_file.read(12)
1542-
h = zd(header[0:12])
1543-
if zinfo.flag_bits & 0x8:
1544-
# compare against the file type from extended local headers
1545-
check_byte = (zinfo._raw_time >> 8) & 0xff
1546-
else:
1547-
# compare against the CRC otherwise
1548-
check_byte = (zinfo.CRC >> 24) & 0xff
1549-
if h[11] != check_byte:
1550-
raise RuntimeError("Bad password for file %r" % name)
1551-
1552-
return ZipExtFile(zef_file, mode, zinfo, zd, True)
1557+
return ZipExtFile(zef_file, mode, zinfo, pwd, True)
15531558
except:
15541559
zef_file.close()
15551560
raise
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fixed seeking backward on an encrypted :class:`zipfile.ZipExtFile`.

0 commit comments

Comments
 (0)