Skip to content

Commit ed2db31

Browse files
bpo-38334: Fix seeking backward on an encrypted zipfile.ZipExtFile. (pythonGH-16937)
Test by Daniel Hillier. (cherry picked from commit 5c32af7) Co-authored-by: Serhiy Storchaka <storchaka@gmail.com>
1 parent baf2657 commit ed2db31

3 files changed

Lines changed: 70 additions & 26 deletions

File tree

Lib/test/test_zipfile.py

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

1877+
def test_seek_tell(self):
1878+
self.zip.setpassword(b"python")
1879+
txt = self.plain
1880+
test_word = b'encryption'
1881+
bloc = txt.find(test_word)
1882+
bloc_len = len(test_word)
1883+
with self.zip.open("test.txt", "r") as fp:
1884+
fp.seek(bloc, os.SEEK_SET)
1885+
self.assertEqual(fp.tell(), bloc)
1886+
fp.seek(-bloc, os.SEEK_CUR)
1887+
self.assertEqual(fp.tell(), 0)
1888+
fp.seek(bloc, os.SEEK_CUR)
1889+
self.assertEqual(fp.tell(), bloc)
1890+
self.assertEqual(fp.read(bloc_len), txt[bloc:bloc+bloc_len])
1891+
1892+
# Make sure that the second read after seeking back beyond
1893+
# _readbuffer returns the same content (ie. rewind to the start of
1894+
# the file to read forward to the required position).
1895+
old_read_size = fp.MIN_READ_SIZE
1896+
fp.MIN_READ_SIZE = 1
1897+
fp._readbuffer = b''
1898+
fp._offset = 0
1899+
fp.seek(0, os.SEEK_SET)
1900+
self.assertEqual(fp.tell(), 0)
1901+
fp.seek(bloc, os.SEEK_CUR)
1902+
self.assertEqual(fp.read(bloc_len), txt[bloc:bloc+bloc_len])
1903+
fp.MIN_READ_SIZE = old_read_size
1904+
1905+
fp.seek(0, os.SEEK_END)
1906+
self.assertEqual(fp.tell(), len(txt))
1907+
fp.seek(0, os.SEEK_SET)
1908+
self.assertEqual(fp.tell(), 0)
1909+
1910+
# Read the file completely to definitely call any eof integrity
1911+
# checks (crc) and make sure they still pass.
1912+
fp.read()
1913+
1914+
18771915
class AbstractTestsWithRandomBinaryFiles:
18781916
@classmethod
18791917
def setUpClass(cls):

Lib/zipfile.py

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

787-
def __init__(self, fileobj, mode, zipinfo, decrypter=None,
787+
def __init__(self, fileobj, mode, zipinfo, pwd=None,
788788
close_fileobj=False):
789789
self._fileobj = fileobj
790-
self._decrypter = decrypter
790+
self._pwd = pwd
791791
self._close_fileobj = close_fileobj
792792

793793
self._compress_type = zipinfo.compress_type
@@ -802,11 +802,6 @@ def __init__(self, fileobj, mode, zipinfo, decrypter=None,
802802

803803
self.newlines = None
804804

805-
# Adjust read size for encrypted files since the first 12 bytes
806-
# are for the encryption/password information.
807-
if self._decrypter is not None:
808-
self._compress_left -= 12
809-
810805
self.mode = mode
811806
self.name = zipinfo.filename
812807

@@ -827,6 +822,30 @@ def __init__(self, fileobj, mode, zipinfo, decrypter=None,
827822
except AttributeError:
828823
pass
829824

825+
self._decrypter = None
826+
if pwd:
827+
if zipinfo.flag_bits & 0x8:
828+
# compare against the file type from extended local headers
829+
check_byte = (zipinfo._raw_time >> 8) & 0xff
830+
else:
831+
# compare against the CRC otherwise
832+
check_byte = (zipinfo.CRC >> 24) & 0xff
833+
h = self._init_decrypter()
834+
if h != check_byte:
835+
raise RuntimeError("Bad password for file %r" % zipinfo.orig_filename)
836+
837+
838+
def _init_decrypter(self):
839+
self._decrypter = _ZipDecrypter(self._pwd)
840+
# The first 12 bytes in the cypher stream is an encryption header
841+
# used to strengthen the algorithm. The first 11 bytes are
842+
# completely random, while the 12th contains the MSB of the CRC,
843+
# or the MSB of the file time depending on the header type
844+
# and is used to check the correctness of the password.
845+
header = self._fileobj.read(12)
846+
self._compress_left -= 12
847+
return self._decrypter(header)[11]
848+
830849
def __repr__(self):
831850
result = ['<%s.%s' % (self.__class__.__module__,
832851
self.__class__.__qualname__)]
@@ -1053,6 +1072,8 @@ def seek(self, offset, whence=0):
10531072
self._decompressor = _get_decompressor(self._compress_type)
10541073
self._eof = False
10551074
read_offset = new_pos
1075+
if self._decrypter is not None:
1076+
self._init_decrypter()
10561077

10571078
while read_offset > 0:
10581079
read_len = min(self.MAX_SEEK_READ, read_offset)
@@ -1515,32 +1536,16 @@ def open(self, name, mode="r", pwd=None, *, force_zip64=False):
15151536

15161537
# check for encrypted flag & handle password
15171538
is_encrypted = zinfo.flag_bits & 0x1
1518-
zd = None
15191539
if is_encrypted:
15201540
if not pwd:
15211541
pwd = self.pwd
15221542
if not pwd:
15231543
raise RuntimeError("File %r is encrypted, password "
15241544
"required for extraction" % name)
1545+
else:
1546+
pwd = None
15251547

1526-
zd = _ZipDecrypter(pwd)
1527-
# The first 12 bytes in the cypher stream is an encryption header
1528-
# used to strengthen the algorithm. The first 11 bytes are
1529-
# completely random, while the 12th contains the MSB of the CRC,
1530-
# or the MSB of the file time depending on the header type
1531-
# and is used to check the correctness of the password.
1532-
header = zef_file.read(12)
1533-
h = zd(header[0:12])
1534-
if zinfo.flag_bits & 0x8:
1535-
# compare against the file type from extended local headers
1536-
check_byte = (zinfo._raw_time >> 8) & 0xff
1537-
else:
1538-
# compare against the CRC otherwise
1539-
check_byte = (zinfo.CRC >> 24) & 0xff
1540-
if h[11] != check_byte:
1541-
raise RuntimeError("Bad password for file %r" % name)
1542-
1543-
return ZipExtFile(zef_file, mode, zinfo, zd, True)
1548+
return ZipExtFile(zef_file, mode, zinfo, pwd, True)
15441549
except:
15451550
zef_file.close()
15461551
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)