From: Michihiro NAKAJIMA Date: Fri, 15 Aug 2014 21:24:48 +0000 (+0900) Subject: Add support for Traditional PKWARE decryption to zip reader. X-Git-Tag: v3.1.900a~236 X-Git-Url: http://git.ipfire.org/cgi-bin/gitweb.cgi?a=commitdiff_plain;h=8145520c0fe01aec836cff2529a0356794896a0c;p=thirdparty%2Flibarchive.git Add support for Traditional PKWARE decryption to zip reader. --- diff --git a/Makefile.am b/Makefile.am index 5982f64b6..aa21edd52 100644 --- a/Makefile.am +++ b/Makefile.am @@ -461,6 +461,7 @@ libarchive_test_SOURCES= \ libarchive/test/test_read_format_zip_nofiletype.c \ libarchive/test/test_read_format_zip_padded.c \ libarchive/test/test_read_format_zip_sfx.c \ + libarchive/test/test_read_format_zip_traditional_encryption_data.c \ libarchive/test/test_read_format_zip_zip64.c \ libarchive/test/test_read_large.c \ libarchive/test/test_read_pax_truncated.c \ @@ -757,6 +758,7 @@ libarchive_test_EXTRA_DIST=\ libarchive/test/test_read_format_zip_padded3.zip.uu \ libarchive/test/test_read_format_zip_sfx.uu \ libarchive/test/test_read_format_zip_symlink.zip.uu \ + libarchive/test/test_read_format_zip_traditional_encryption_data.zip.uu \ libarchive/test/test_read_format_zip_ux.zip.uu \ libarchive/test/test_read_format_zip_zip64a.zip.uu \ libarchive/test/test_read_format_zip_zip64b.zip.uu \ diff --git a/libarchive/archive_read_support_format_zip.c b/libarchive/archive_read_support_format_zip.c index 65dc81784..093037f7f 100644 --- a/libarchive/archive_read_support_format_zip.c +++ b/libarchive/archive_read_support_format_zip.c @@ -82,6 +82,11 @@ struct zip_entry { unsigned char compression; unsigned char system; /* From "version written by" */ unsigned char flags; /* Our extra markers. */ + unsigned char decdat;/* Used for Decryption check */ +}; + +struct trad_enc_ctx { + uint32_t keys[3]; }; /* Bits used in zip_flags. */ @@ -143,6 +148,13 @@ struct zip { struct archive_string_conv *sconv_utf8; int init_default_conversion; int process_mac_extensions; + + struct archive_string password; + struct trad_enc_ctx tctx; + char tctx_valid; + unsigned char *decrypted_buffer; + size_t decrypted_buffer_size; + size_t decrypted_bytes_remaining; }; /* Many systems define min or MIN, but not all. */ @@ -150,6 +162,67 @@ struct zip { /* ------------------------------------------------------------------------ */ +/* + Traditional PKWARE Decryption functions. + */ + +static void +trad_enc_update_keys(struct trad_enc_ctx *ctx, uint8_t c) +{ + uint8_t t; +#define CRC32(c, b) (crc32(c ^ 0xffffffffUL, &b, 1) ^ 0xffffffffUL) + + ctx->keys[0] = CRC32(ctx->keys[0], c); + ctx->keys[1] = (ctx->keys[1] + (ctx->keys[0] & 0xff)) * 134775813L + 1; + t = (ctx->keys[1] >> 24) & 0xff; + ctx->keys[2] = CRC32(ctx->keys[2], t); +#undef CRC32 +} + +static void +trad_enc_init(struct trad_enc_ctx *ctx, const char *p, size_t l) +{ + ctx->keys[0] = 305419896L; + ctx->keys[1] = 591751049L; + ctx->keys[2] = 878082192L; + + for (;l; --l) + trad_enc_update_keys(ctx, *p++); +} + +static uint8_t +trad_enc_decypt_byte(struct trad_enc_ctx *ctx) +{ + unsigned temp = ctx->keys[2] | 2; + return (uint8_t)((temp * (temp ^ 1)) >> 8) & 0xff; +} + +static void +trad_enc_decrypt(struct trad_enc_ctx *ctx, uint8_t *p, size_t l) +{ + + for (; l; --l) { + uint8_t t = *p ^ trad_enc_decypt_byte(ctx); + *p++ = t; + trad_enc_update_keys(ctx, t); + } +} + +static unsigned int +trad_enc_read_encyption_header(struct trad_enc_ctx *ctx, const uint8_t *p, + size_t l) +{ + uint8_t header[12]; + + if (l < 12) + return -1; + + memcpy(header, p, 12); + trad_enc_decrypt(ctx, header, 12); + /* Return the last byte for CRC check. */ + return header[11]; +} + /* * Common code for streaming or seeking modes. * @@ -546,6 +619,10 @@ zip_read_local_file_header(struct archive_read *a, struct archive_entry *entry, zip_entry->compression = (char)archive_le16dec(p + 8); zip_entry->mtime = zip_time(p + 10); zip_entry->crc32 = archive_le32dec(p + 14); + if (zip_entry->zip_flags & ZIP_LENGTH_AT_END) + zip_entry->decdat = p[11]; + else + zip_entry->decdat = p[17]; zip_entry->compressed_size = archive_le32dec(p + 18); zip_entry->uncompressed_size = archive_le32dec(p + 22); filename_length = archive_le16dec(p + 26); @@ -864,6 +941,16 @@ zip_read_data_none(struct archive_read *a, const void **_buff, if (bytes_avail > zip->entry_bytes_remaining) bytes_avail = (ssize_t)zip->entry_bytes_remaining; } + if (zip->tctx_valid) { + size_t dec_size = bytes_avail; + + if (dec_size > zip->decrypted_buffer_size) + dec_size = zip->decrypted_buffer_size; + memcpy(zip->decrypted_buffer, buff, dec_size); + trad_enc_decrypt(&zip->tctx, zip->decrypted_buffer, dec_size); + bytes_avail = dec_size; + buff = (const char *)zip->decrypted_buffer; + } *size = bytes_avail; zip->entry_bytes_remaining -= bytes_avail; zip->entry_uncompressed_bytes_read += bytes_avail; @@ -945,6 +1032,27 @@ zip_read_data_deflate(struct archive_read *a, const void **buff, return (ARCHIVE_FATAL); } + if (zip->tctx_valid) { + size_t buff_remaining = + zip->decrypted_buffer_size - zip->decrypted_bytes_remaining; + + if (buff_remaining > (size_t)bytes_avail) + buff_remaining = (size_t)bytes_avail; + if ((int64_t)(zip->decrypted_bytes_remaining + buff_remaining) + > zip->entry_bytes_remaining) + buff_remaining = zip->entry_bytes_remaining + - zip->decrypted_bytes_remaining; + memcpy(zip->decrypted_buffer + zip->decrypted_bytes_remaining, + compressed_buff, buff_remaining); + __archive_read_consume(a, buff_remaining); + trad_enc_decrypt(&zip->tctx, + zip->decrypted_buffer + zip->decrypted_bytes_remaining, + buff_remaining); + zip->decrypted_bytes_remaining += buff_remaining; + bytes_avail = zip->decrypted_bytes_remaining; + compressed_buff = (const char *)zip->decrypted_buffer; + } + /* * A bug in zlib.h: stream.next_in should be marked 'const' * but isn't (the library never alters data through the @@ -977,7 +1085,10 @@ zip_read_data_deflate(struct archive_read *a, const void **buff, /* Consume as much as the compressor actually used. */ bytes_avail = zip->stream.total_in; - __archive_read_consume(a, bytes_avail); + if (zip->tctx_valid) + zip->decrypted_bytes_remaining -= bytes_avail; + else + __archive_read_consume(a, bytes_avail); zip->entry_bytes_remaining -= bytes_avail; zip->entry_compressed_bytes_read += bytes_avail; @@ -1041,16 +1152,73 @@ archive_read_format_zip_read_data(struct archive_read *a, if (AE_IFREG != (zip->entry->mode & AE_IFMT)) return (ARCHIVE_EOF); - if (zip->entry->zip_flags & (ZIP_ENCRYPTED | ZIP_STRONG_ENCRYPTED)) { - zip->has_encrypted_entries = 1; - archive_set_error(&a->archive, ARCHIVE_ERRNO_FILE_FORMAT, - "Encrypted file is unsupported"); - return (ARCHIVE_FAILED); - } - __archive_read_consume(a, zip->unconsumed); zip->unconsumed = 0; + if (!zip->tctx_valid && + zip->entry->zip_flags & (ZIP_ENCRYPTED | ZIP_STRONG_ENCRYPTED)) { + zip->has_encrypted_entries = 1; + if (zip->entry->zip_flags & ZIP_STRONG_ENCRYPTED) { + archive_set_error(&a->archive, + ARCHIVE_ERRNO_FILE_FORMAT, + "Encrypted file is unsupported"); + return (ARCHIVE_FAILED); + } else if (archive_strlen(&zip->password) == 0) { + archive_set_error(&a->archive, + ARCHIVE_ERRNO_MISC, + "This is encrypted file and passowrd is needed"); + return (ARCHIVE_FAILED); + } + + /* + * Initialize ctx for Traditional PKWARE Decyption. + */ + if (!zip->tctx_valid) { + const void *p; + unsigned int crcchk; + + trad_enc_init(&zip->tctx, zip->password.s, + archive_strlen(&zip->password)); + + /* + Read the 12 bytes encryption header stored at + the start of the data area. + */ +#define ENC_HEADER_SIZE 12 + p = __archive_read_ahead(a, ENC_HEADER_SIZE, NULL); + if (p == NULL) { + archive_set_error(&a->archive, + ARCHIVE_ERRNO_FILE_FORMAT, + "Truncated ZIP file data"); + return (ARCHIVE_FATAL); + } + crcchk = trad_enc_read_encyption_header(&zip->tctx, p, + ENC_HEADER_SIZE); + if (crcchk != zip->entry->decdat) { + archive_set_error(&a->archive, + ARCHIVE_ERRNO_MISC, "Incorrect passowrd"); + return (ARCHIVE_FAILED); + } + __archive_read_consume(a, ENC_HEADER_SIZE); + zip->tctx_valid = 1; + zip->entry_bytes_remaining -= ENC_HEADER_SIZE; + //zip->entry_uncompressed_bytes_read += ENC_HEADER_SIZE; + zip->entry_compressed_bytes_read += ENC_HEADER_SIZE; + if (zip->decrypted_buffer == NULL) { + size_t bs = 256 * 1024; + zip->decrypted_buffer_size = bs; + zip->decrypted_buffer = malloc(bs); + if (zip->decrypted_buffer == NULL) { + archive_set_error(&a->archive, ENOMEM, + "No memory for ZIP decryption"); + return (ARCHIVE_FATAL); + } + } + zip->decrypted_bytes_remaining = 0; +#undef ENC_HEADER_SIZE + } + } + switch(zip->entry->compression) { case 0: /* No compression. */ r = zip_read_data_none(a, buff, size, offset); @@ -1134,6 +1302,8 @@ archive_read_format_zip_cleanup(struct archive_read *a) zip_entry = next_zip_entry; } } + archive_string_free(&zip->password); + free(zip->decrypted_buffer); free(zip); (a->format->data) = NULL; return (ARCHIVE_OK); @@ -1192,6 +1362,12 @@ archive_read_format_zip_options(struct archive_read *a, } else if (strcmp(key, "mac-ext") == 0) { zip->process_mac_extensions = (val != NULL && val[0] != 0); return (ARCHIVE_OK); + } else if (strcmp(key, "password") == 0) { + if (val != NULL) + archive_strcpy(&zip->password, val); + else + archive_string_empty(&zip->password); + return (ARCHIVE_OK); } /* Note: The "warn" return is just to inform the options @@ -1295,6 +1471,7 @@ archive_read_format_zip_streamable_read_header(struct archive_read *a, } zip->entry = zip->zip_entries; memset(zip->entry, 0, sizeof(struct zip_entry)); + zip->tctx_valid = 0; /* Search ahead for the next local file header. */ __archive_read_consume(a, zip->unconsumed); @@ -1435,13 +1612,12 @@ archive_read_support_format_zip_streamable(struct archive *_a) archive_check_magic(_a, ARCHIVE_READ_MAGIC, ARCHIVE_STATE_NEW, "archive_read_support_format_zip"); - zip = (struct zip *)malloc(sizeof(*zip)); + zip = (struct zip *)calloc(1, sizeof(*zip)); if (zip == NULL) { archive_set_error(&a->archive, ENOMEM, "Can't allocate zip data"); return (ARCHIVE_FATAL); } - memset(zip, 0, sizeof(*zip)); /* Streamable reader doesn't support mac extensions. */ zip->process_mac_extensions = 0; @@ -1813,6 +1989,10 @@ slurp_central_directory(struct archive_read *a, struct zip *zip) zip_entry->compression = (char)archive_le16dec(p + 10); zip_entry->mtime = zip_time(p + 12); zip_entry->crc32 = archive_le32dec(p + 16); + if (zip_entry->zip_flags & ZIP_LENGTH_AT_END) + zip_entry->decdat = p[13]; + else + zip_entry->decdat = p[19]; zip_entry->compressed_size = archive_le32dec(p + 20); zip_entry->uncompressed_size = archive_le32dec(p + 24); filename_length = archive_le16dec(p + 28); @@ -2109,6 +2289,8 @@ archive_read_format_zip_seekable_read_header(struct archive_read *a, else rsrc = NULL; + zip->tctx_valid = 0; + /* File entries are sorted by the header offset, we should mostly * use __archive_read_consume to advance a read point to avoid redundant * data reading. */ @@ -2156,13 +2338,12 @@ archive_read_support_format_zip_seekable(struct archive *_a) archive_check_magic(_a, ARCHIVE_READ_MAGIC, ARCHIVE_STATE_NEW, "archive_read_support_format_zip_seekable"); - zip = (struct zip *)malloc(sizeof(*zip)); + zip = (struct zip *)calloc(1, sizeof(*zip)); if (zip == NULL) { archive_set_error(&a->archive, ENOMEM, "Can't allocate zip data"); return (ARCHIVE_FATAL); } - memset(zip, 0, sizeof(*zip)); #ifdef HAVE_COPYFILE_H /* Set this by default on Mac OS. */ diff --git a/libarchive/test/CMakeLists.txt b/libarchive/test/CMakeLists.txt index 359a90f1f..c496d5654 100644 --- a/libarchive/test/CMakeLists.txt +++ b/libarchive/test/CMakeLists.txt @@ -161,6 +161,7 @@ IF(ENABLE_TEST) test_read_format_zip_nofiletype.c test_read_format_zip_padded.c test_read_format_zip_sfx.c + test_read_format_zip_traditional_encryption_data.c test_read_format_zip_zip64.c test_read_large.c test_read_pax_truncated.c diff --git a/libarchive/test/test_read_format_zip_traditional_encryption_data.c b/libarchive/test/test_read_format_zip_traditional_encryption_data.c new file mode 100644 index 000000000..8234c6377 --- /dev/null +++ b/libarchive/test/test_read_format_zip_traditional_encryption_data.c @@ -0,0 +1,134 @@ +/*- + * Copyright (c) 2013 Konrad Kleine + * Copyright (c) 2014 Michihiro NAKAJIMA + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR(S) ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR(S) BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +#include "test.h" +__FBSDID("$FreeBSD$"); + +DEFINE_TEST(test_read_format_zip_traditional_encryption_data) +{ + /* This file is password protected (Traditional PKWARE Enctypted). + The headers are NOT encrypted. Password is "12345678". */ + const char *refname = + "test_read_format_zip_traditional_encryption_data.zip"; + struct archive_entry *ae; + struct archive *a; + char buff[512]; + + extract_reference_file(refname); + + /* + * Extract a zip file without password. + */ + assert((a = archive_read_new()) != NULL); + assertEqualIntA(a, ARCHIVE_OK, archive_read_support_filter_all(a)); + assertEqualIntA(a, ARCHIVE_OK, archive_read_support_format_all(a)); + assertEqualIntA(a, ARCHIVE_OK, + archive_read_open_filename(a, refname, 10240)); + + assertEqualIntA(a, ARCHIVE_READ_FORMAT_ENCRYPTION_DONT_KNOW, + archive_read_has_encrypted_entries(a)); + + /* Verify encrypted file "bar.txt" */ + assertEqualIntA(a, ARCHIVE_OK, archive_read_next_header(a, &ae)); + assertEqualInt((AE_IFREG | 0644), archive_entry_mode(ae)); + assertEqualString("bar.txt", archive_entry_pathname(ae)); + assertEqualInt(495, archive_entry_size(ae)); + assertEqualInt(1, archive_entry_is_data_encrypted(ae)); + assertEqualInt(0, archive_entry_is_metadata_encrypted(ae)); + assertEqualIntA(a, 1, archive_read_has_encrypted_entries(a)); + assertEqualInt(ARCHIVE_FAILED, archive_read_data(a, buff, sizeof(buff))); + + /* Verify encrypted file "foo.txt" */ + assertEqualIntA(a, ARCHIVE_OK, archive_read_next_header(a, &ae)); + assertEqualInt((AE_IFREG | 0644), archive_entry_mode(ae)); + assertEqualString("foo.txt", archive_entry_pathname(ae)); + assertEqualInt(495, archive_entry_size(ae)); + assertEqualInt(1, archive_entry_is_data_encrypted(ae)); + assertEqualInt(0, archive_entry_is_metadata_encrypted(ae)); + assertEqualIntA(a, 1, archive_read_has_encrypted_entries(a)); + assertEqualInt(ARCHIVE_FAILED, archive_read_data(a, buff, sizeof(buff))); + + assertEqualInt(2, archive_file_count(a)); + + /* End of archive. */ + assertEqualIntA(a, ARCHIVE_EOF, archive_read_next_header(a, &ae)); + + /* Verify archive format. */ + assertEqualIntA(a, ARCHIVE_FILTER_NONE, archive_filter_code(a, 0)); + assertEqualIntA(a, ARCHIVE_FORMAT_ZIP, archive_format(a)); + + /* Close the archive. */ + assertEqualInt(ARCHIVE_OK, archive_read_close(a)); + assertEqualInt(ARCHIVE_OK, archive_read_free(a)); + + + /* + * Extract a zip file with password. + */ + assert((a = archive_read_new()) != NULL); + assertEqualIntA(a, ARCHIVE_OK, archive_read_support_filter_all(a)); + assertEqualIntA(a, ARCHIVE_OK, archive_read_support_format_all(a)); + assertEqualIntA(a, ARCHIVE_OK, + archive_read_set_options(a, "zip:password=12345678")); + assertEqualIntA(a, ARCHIVE_OK, + archive_read_open_filename(a, refname, 10240)); + + assertEqualIntA(a, ARCHIVE_READ_FORMAT_ENCRYPTION_DONT_KNOW, + archive_read_has_encrypted_entries(a)); + + /* Verify encrypted file "bar.txt" */ + assertEqualIntA(a, ARCHIVE_OK, archive_read_next_header(a, &ae)); + assertEqualInt((AE_IFREG | 0644), archive_entry_mode(ae)); + assertEqualString("bar.txt", archive_entry_pathname(ae)); + assertEqualInt(495, archive_entry_size(ae)); + assertEqualInt(1, archive_entry_is_data_encrypted(ae)); + assertEqualInt(0, archive_entry_is_metadata_encrypted(ae)); + assertEqualIntA(a, 1, archive_read_has_encrypted_entries(a)); + assertEqualInt(495, archive_read_data(a, buff, sizeof(buff))); + + /* Verify encrypted file "foo.txt" */ + assertEqualIntA(a, ARCHIVE_OK, archive_read_next_header(a, &ae)); + assertEqualInt((AE_IFREG | 0644), archive_entry_mode(ae)); + assertEqualString("foo.txt", archive_entry_pathname(ae)); + assertEqualInt(495, archive_entry_size(ae)); + assertEqualInt(1, archive_entry_is_data_encrypted(ae)); + assertEqualInt(0, archive_entry_is_metadata_encrypted(ae)); + assertEqualIntA(a, 1, archive_read_has_encrypted_entries(a)); + assertEqualInt(495, archive_read_data(a, buff, sizeof(buff))); + + assertEqualInt(2, archive_file_count(a)); + + /* End of archive. */ + assertEqualIntA(a, ARCHIVE_EOF, archive_read_next_header(a, &ae)); + + /* Verify archive format. */ + assertEqualIntA(a, ARCHIVE_FILTER_NONE, archive_filter_code(a, 0)); + assertEqualIntA(a, ARCHIVE_FORMAT_ZIP, archive_format(a)); + + /* Close the archive. */ + assertEqualInt(ARCHIVE_OK, archive_read_close(a)); + assertEqualInt(ARCHIVE_OK, archive_read_free(a)); +} + diff --git a/libarchive/test/test_read_format_zip_traditional_encryption_data.zip.uu b/libarchive/test/test_read_format_zip_traditional_encryption_data.zip.uu new file mode 100644 index 000000000..3e418b2c9 --- /dev/null +++ b/libarchive/test/test_read_format_zip_traditional_encryption_data.zip.uu @@ -0,0 +1,12 @@ +begin 644 test_read_format_zip_traditional_encryption_data.zip +M4$L#!!0`"0`(`,HT$$5'_=BD'0```.\!```'`!P`8F%R+G1X=%54"0`#S'WN +M4])][E-U>`L``03U`0``!!0```"E=VLFG3$F"T]Q^;J:A17F^=#3L<1CO8K; +MX-(?KU!+!PA'_=BD'0```.\!``!02P,$%``)``@`V#001>O'D[0=````[P$` +M``<`'`!F;V\N='AT550)``/H?>Y3Z'WN4W5X"P`!!/4!```$%````#O!PFX- +MNJG:A01W(N8M^T7N9=\_D!=4=?,$"6L\4$L'".O'D[0=````[P$``%!+`0(> +M`Q0`"0`(`,HT$$5'_=BD'0```.\!```'`!@```````$```"D@0````!B87(N +M='AT550%``/,?>Y3=7@+``$$]0$```04````4$L!`AX#%``)``@`V#001>O' +MD[0=````[P$```<`&````````0```*2!;@```&9O;RYT>'155`4``^A][E-U +D>`L``03U`0``!!0```!02P4&``````(``@":````W``````` +` +end