diff --git a/include/Arguments.hpp b/include/Arguments.hpp index 76251e0..e7eef21 100644 --- a/include/Arguments.hpp +++ b/include/Arguments.hpp @@ -70,6 +70,9 @@ class Arguments /// Tell whether to keep the encryption header or discard it when writing the deciphered text bool keepHeader = false; + /// File to write an unencrypted copy of the encrypted archive + std::optional decryptedArchive; + /// Arguments needed to change an archive's password struct ChangePassword { @@ -152,6 +155,7 @@ class Arguments keys, decipheredFile, keepHeader, + decryptedArchive, changePassword, changeKeys, bruteforce, diff --git a/include/Zip.hpp b/include/Zip.hpp index 281cb3d..002ae6f 100644 --- a/include/Zip.hpp +++ b/include/Zip.hpp @@ -171,6 +171,10 @@ class Zip /// \exception Error if the archive is not a valid zip archive void changeKeys(std::ostream& os, const Keys& oldKeys, const Keys& newKeys, Progress& progress) const; + /// \brief Copy the zip file into \a os removing encryption using the given keys + /// \exception Error if the archive is not a valid zip archive + void decrypt(std::ostream& os, const Keys& keys, Progress& progress) const; + private: std::optional m_file; // optionally own the stream std::istream& m_is; diff --git a/src/Arguments.cpp b/src/Arguments.cpp index 164cbb2..0a61bf1 100644 --- a/src/Arguments.cpp +++ b/src/Arguments.cpp @@ -97,8 +97,8 @@ Arguments::Arguments(int argc, const char* argv[]) // check constraints on arguments if (keys) { - if (!decipheredFile && !changePassword && !changeKeys && !bruteforce) - throw Error{"-d, -U, --change-keys or --bruteforce parameter is missing (required by -k)"}; + if (!decipheredFile && !decryptedArchive && !changePassword && !changeKeys && !bruteforce) + throw Error{"-d, -D, -U, --change-keys or --bruteforce parameter is missing (required by -k)"}; } else if (!password) { @@ -131,6 +131,11 @@ Arguments::Arguments(int argc, const char* argv[]) if (decipheredFile && !cipherArchive && decipheredFile == cipherFile) throw Error{"-c and -d parameters must point to different files"}; + if (decryptedArchive && !cipherArchive) + throw Error{"-C parameter is missing (required by -D)"}; + if (decryptedArchive && decryptedArchive == cipherArchive) + throw Error{"-C and -D parameters must point to different files"}; + if (changePassword && !cipherArchive) throw Error{"-C parameter is missing (required by -U)"}; if (changePassword && changePassword->unlockedArchive == cipherArchive) @@ -250,6 +255,9 @@ void Arguments::parseArgument() case Option::keepHeader: keepHeader = true; break; + case Option::decryptedArchive: + decryptedArchive = readString("decipheredzip"); + break; case Option::changePassword: changePassword = {readString("unlockedzip"), readString("password")}; break; @@ -338,6 +346,7 @@ auto Arguments::readOption(const std::string& description) -> Arguments::Option PAIRS(-k, --keys, keys), PAIRS(-d, --decipher, decipheredFile), PAIR ( --keep-header, keepHeader), + PAIRS(-D, --decrypt, decryptedArchive), PAIRS(-U, --change-password, changePassword), PAIR ( --change-keys, changeKeys), PAIRS(-b, --bruteforce, bruteforce), diff --git a/src/Zip.cpp b/src/Zip.cpp index c0db526..f2f44e1 100644 --- a/src/Zip.cpp +++ b/src/Zip.cpp @@ -1,17 +1,19 @@ #include "Zip.hpp" +#include "Data.hpp" #include "file.hpp" #include #include #include #include +#include namespace { template -auto read(std::istream& is) -> T +auto readInt(std::istream& is) -> T { // We make no assumption about platform endianness. auto x = T{}; @@ -21,7 +23,15 @@ auto read(std::istream& is) -> T return x; } -auto read(std::istream& is, std::size_t length) -> std::string +template +void writeInt(std::ostream& os, const T& x) +{ + // We make no assumption about platform endianness. + for (auto index = std::size_t{}; index < sizeof(T); index++) + os.put(lsb(x >> (8 * index))); +} + +auto readString(std::istream& is, std::size_t length) -> std::string { auto string = std::string{}; string.resize(length); @@ -29,17 +39,388 @@ auto read(std::istream& is, std::size_t length) -> std::string return string; } -template -void write(std::ostream& os, const T& x) +void writeString(std::ostream& os, const std::string& string) { - // We make no assumption about platform endianness. - for (auto index = std::size_t{}; index < sizeof(T); index++) - os.put(lsb(x >> (8 * index))); + os.write(string.data(), string.size()); } +struct LocalFileHeader; +struct CentralDirectoryHeader; + +struct ExtraField +{ + struct Aes + { + static constexpr auto headerId = std::uint16_t{0x9901}; + + static constexpr auto getDataSize() -> std::uint16_t + { + return 7; + } + + std::uint16_t version; + std::uint16_t vendor; + std::uint8_t strength; + std::uint16_t method; + + static auto read(std::istream& is, std::uint16_t dataSize) -> Aes + { + auto data = Aes{}; + if (dataSize == getDataSize()) + { + data.version = readInt(is); + data.vendor = readInt(is); + data.strength = readInt(is); + data.method = readInt(is); + } + if (!is || dataSize != getDataSize()) + throw Zip::Error{"could not read AES extra field"}; + return data; + } + + void writeData(std::ostream& os) const + { + writeInt(os, version); + writeInt(os, vendor); + writeInt(os, strength); + writeInt(os, method); + } + }; + + struct InfoZipUnicodePath + { + static constexpr auto headerId = std::uint16_t{0x7075}; + + auto getDataSize() const -> std::uint16_t + { + return 5 + unicodeName.size(); + } + + std::uint8_t version; + std::uint32_t nameCrc32; + std::string unicodeName; + + static auto read(std::istream& is, std::uint16_t dataSize) -> InfoZipUnicodePath + { + auto data = InfoZipUnicodePath{}; + if (5 <= dataSize) + { + data.version = readInt(is); + data.nameCrc32 = readInt(is); + data.unicodeName = readString(is, dataSize - 5); + } + if (!is || dataSize != data.getDataSize()) + throw Zip::Error{"could not read Info-Zip Unicode Path extra field"}; + return data; + } + + void writeData(std::ostream& os) const + { + writeInt(os, version); + writeInt(os, nameCrc32); + writeString(os, unicodeName); + } + }; + + struct Zip64 + { + static constexpr auto headerId = std::uint16_t{0x0001}; + + auto getDataSize() const -> std::uint16_t + { + return (uncompressedSize ? 8 : 0) + (compressedSize ? 8 : 0) + (headerOffset ? 8 : 0) + + (diskStartNumber ? 4 : 0); + } + + std::optional uncompressedSize; + std::optional compressedSize; + std::optional headerOffset; + std::optional diskStartNumber; + + template || + std::is_same_v>> + static auto read(std::istream& is, std::uint16_t dataSize, const Header& header) -> Zip64 + { + auto data = Zip64{}; + auto remaining = dataSize; + if (8 <= remaining && header.uncompressedSize == mask<0, 32>) + { + data.uncompressedSize = readInt(is); + remaining -= 8; + } + if (8 <= remaining && header.compressedSize == mask<0, 32>) + { + data.compressedSize = readInt(is); + remaining -= 8; + } + if constexpr (std::is_same_v) + { + if (8 <= remaining && header.headerOffset == mask<0, 32>) + { + data.headerOffset = readInt(is); + remaining -= 8; + } + if (4 <= remaining && header.diskStartNumber == mask<0, 16>) + { + data.diskStartNumber = readInt(is); + remaining -= 4; + } + } + + if (!is || dataSize != data.getDataSize()) + throw Zip::Error{"could not read ZIP64 extra field"}; + + return data; + } + + void writeData(std::ostream& os) const + { + if (uncompressedSize) + writeInt(os, *uncompressedSize); + if (compressedSize) + writeInt(os, *compressedSize); + if (headerOffset) + writeInt(os, *headerOffset); + if (diskStartNumber) + writeInt(os, *diskStartNumber); + } + }; + + struct Other + { + std::uint16_t headerId; + + auto getDataSize() const -> std::uint16_t + { + return data.size(); + } + + std::string data; + + static auto read(std::istream& is, std::uint16_t headerId, std::uint16_t dataSize) -> Other + { + auto data = Other{headerId, readString(is, dataSize)}; + if (!is || dataSize != data.getDataSize()) + throw Zip::Error{"could not read extra field"}; + return data; + } + + void writeData(std::ostream& os) const + { + writeString(os, data); + } + }; + + template || + std::is_same_v>> + static auto read(std::istream& is, const Header& header) -> ExtraField + { + auto extraField = ExtraField{}; + auto remaining = header.extraFieldLength; + while (remaining) + { + if (remaining < 4) + throw Zip::Error{"could not read extra field"}; + + const auto headerId = readInt(is); + const auto dataSize = readInt(is); + remaining -= 4; + + if (!is || remaining < dataSize) + throw Zip::Error{"could not read extra field"}; + + switch (headerId) + { + case Aes::headerId: + extraField.blocks.emplace_back(Aes::read(is, dataSize)); + break; + case InfoZipUnicodePath::headerId: + extraField.blocks.emplace_back(InfoZipUnicodePath::read(is, dataSize)); + break; + case Zip64::headerId: + extraField.blocks.emplace_back(Zip64::read(is, dataSize, header)); + break; + default: + extraField.blocks.emplace_back(Other::read(is, headerId, dataSize)); + break; + } + remaining -= dataSize; + } + return extraField; + } + + void write(std::ostream& os) const + { + for (const auto& block : blocks) + std::visit( + [&os](const auto& block) + { + writeInt(os, block.headerId); + writeInt(os, block.getDataSize()); + block.writeData(os); + }, + block); + } + + template + auto find() const -> const T* + { + const auto it = std::find_if(blocks.begin(), blocks.end(), + [](const auto& block) { return std::holds_alternative(block); }); + return it != blocks.end() ? &std::get(*it) : nullptr; + } + + template + auto find() -> T* + { + const auto it = std::find_if(blocks.begin(), blocks.end(), + [](const auto& block) { return std::holds_alternative(block); }); + return it != blocks.end() ? &std::get(*it) : nullptr; + } + + std::vector> blocks; +}; + +struct LocalFileHeader +{ + std::uint16_t versionNeededToExtract; + std::uint16_t flags; + std::uint16_t method; + std::uint16_t lastModTime; + std::uint16_t lastModDate; + std::uint32_t crc32; + std::uint32_t compressedSize; + std::uint32_t uncompressedSize; + std::uint16_t filenameLength; + std::uint16_t extraFieldLength; + std::string filename; + ExtraField extraField; + + static auto read(std::istream& is) -> LocalFileHeader + { + auto header = LocalFileHeader{}; + header.versionNeededToExtract = readInt(is); + header.flags = readInt(is); + header.method = readInt(is); + header.lastModTime = readInt(is); + header.lastModDate = readInt(is); + header.crc32 = readInt(is); + header.compressedSize = readInt(is); + header.uncompressedSize = readInt(is); + header.filenameLength = readInt(is); + header.extraFieldLength = readInt(is); + header.filename = readString(is, header.filenameLength); + header.extraField = ExtraField::read(is, header); + + if (!is) + throw Zip::Error{"could not read local file header"}; + + return header; + } + + void write(std::ostream& os) const + { + writeInt(os, versionNeededToExtract); + writeInt(os, flags); + writeInt(os, method); + writeInt(os, lastModTime); + writeInt(os, lastModDate); + writeInt(os, crc32); + writeInt(os, compressedSize); + writeInt(os, uncompressedSize); + writeInt(os, filenameLength); + writeInt(os, extraFieldLength); + writeString(os, filename); + extraField.write(os); + } +}; + +struct CentralDirectoryHeader +{ + std::uint16_t versionMadeBy; + std::uint16_t versionNeededToExtract; + std::uint16_t flags; + std::uint16_t method; + std::uint16_t lastModTime; + std::uint16_t lastModDate; + std::uint32_t crc32; + std::uint32_t compressedSize; + std::uint32_t uncompressedSize; + std::uint16_t filenameLength; + std::uint16_t extraFieldLength; + std::uint16_t fileCommentLength; + std::uint16_t diskStartNumber; + std::uint16_t internalFileAttributes; + std::uint32_t externalFileAttributes; + std::uint32_t headerOffset; + std::string filename; + ExtraField extraField; + std::string fileComment; + + static auto read(std::istream& is) -> CentralDirectoryHeader + { + auto header = CentralDirectoryHeader{}; + header.versionMadeBy = readInt(is); + header.versionNeededToExtract = readInt(is); + header.flags = readInt(is); + header.method = readInt(is); + header.lastModTime = readInt(is); + header.lastModDate = readInt(is); + header.crc32 = readInt(is); + header.compressedSize = readInt(is); + header.uncompressedSize = readInt(is); + header.filenameLength = readInt(is); + header.extraFieldLength = readInt(is); + header.fileCommentLength = readInt(is); + header.diskStartNumber = readInt(is); + header.internalFileAttributes = readInt(is); + header.externalFileAttributes = readInt(is); + header.headerOffset = readInt(is); + header.filename = readString(is, header.filenameLength); + header.extraField = ExtraField::read(is, header); + header.fileComment = readString(is, header.fileCommentLength); + + if (!is) + throw Zip::Error{"could not read central directory header"}; + + return header; + } + + void write(std::ostream& os) const + { + writeInt(os, versionMadeBy); + writeInt(os, versionNeededToExtract); + writeInt(os, flags); + writeInt(os, method); + writeInt(os, lastModTime); + writeInt(os, lastModDate); + writeInt(os, crc32); + writeInt(os, compressedSize); + writeInt(os, uncompressedSize); + writeInt(os, filenameLength); + writeInt(os, extraFieldLength); + writeInt(os, fileCommentLength); + writeInt(os, diskStartNumber); + writeInt(os, internalFileAttributes); + writeInt(os, externalFileAttributes); + writeInt(os, headerOffset); + writeString(os, filename); + extraField.write(os); + writeString(os, fileComment); + } + + auto getEncryption() const -> Zip::Encryption + { + return flags & 1 + ? method == 99 || (flags >> 6) & 1 ? Zip::Encryption::Unsupported : Zip::Encryption::Traditional + : Zip::Encryption::None; + } +}; + enum class Signature : std::uint32_t { LocalFileHeader = 0x04034b50, + DataDescriptor = 0x08074b50, CentralDirectoryHeader = 0x02014b50, Zip64Eocd = 0x06064b50, Zip64EocdLocator = 0x07064b50, @@ -48,7 +429,7 @@ enum class Signature : std::uint32_t auto checkSignature(std::istream& is, const Signature& signature) -> bool { - const auto sig = read(is); + const auto sig = readInt(is); return is && sig == static_cast(signature); } @@ -63,7 +444,7 @@ auto findCentralDirectoryOffset(std::istream& is) -> std::uint64_t do { is.seekg(-22 - commentLength, std::ios::end); - signature = read(is); + signature = readInt(is); } while (is && signature != static_cast(Signature::Eocd) && commentLength++ < mask<0, 16>); if (!is || signature != static_cast(Signature::Eocd)) @@ -72,9 +453,9 @@ auto findCentralDirectoryOffset(std::istream& is) -> std::uint64_t // read end of central directory record { - const auto disk = read(is); + const auto disk = readInt(is); is.seekg(10, std::ios::cur); - centralDirectoryOffset = read(is); + centralDirectoryOffset = readInt(is); if (!is) throw Zip::Error{"could not read end of central directory record"}; @@ -87,7 +468,7 @@ auto findCentralDirectoryOffset(std::istream& is) -> std::uint64_t if (checkSignature(is, Signature::Zip64EocdLocator)) { is.seekg(4, std::ios::cur); - const auto zip64EndOfCentralDirectoryOffset = read(is); + const auto zip64EndOfCentralDirectoryOffset = readInt(is); if (!is) throw Zip::Error{"could not read Zip64 end of central directory locator record"}; @@ -97,9 +478,9 @@ auto findCentralDirectoryOffset(std::istream& is) -> std::uint64_t if (checkSignature(is, Signature::Zip64Eocd)) { is.seekg(10, std::ios::cur); - const auto versionNeededToExtract = read(is); + const auto versionNeededToExtract = readInt(is); is.seekg(32, std::ios::cur); - centralDirectoryOffset = read(is); + centralDirectoryOffset = readInt(is); if (!is) throw Zip::Error{"could not read Zip64 end of central directory record"}; @@ -132,101 +513,24 @@ auto Zip::Iterator::operator++() -> Zip::Iterator& if (!checkSignature(*m_is, Signature::CentralDirectoryHeader)) return *this = Iterator{}; - m_is->seekg(4, std::ios::cur); - const auto flags = read(*m_is); - const auto method = read(*m_is); - const auto lastModTime = read(*m_is); - m_is->seekg(2, std::ios::cur); - m_entry->crc32 = read(*m_is); - m_entry->packedSize = read(*m_is); - m_entry->uncompressedSize = read(*m_is); - const auto filenameLength = read(*m_is); - const auto extraFieldLength = read(*m_is); - const auto fileCommentLength = read(*m_is); - m_is->seekg(8, std::ios::cur); - m_entry->offset = read(*m_is); - m_entry->name = read(*m_is, filenameLength); - - m_entry->encryption = flags & 1 - ? method == 99 || (flags >> 6) & 1 ? Encryption::Unsupported : Encryption::Traditional - : Encryption::None; - - m_entry->compression = static_cast(method); - - m_entry->checkByte = (flags >> 3) & 1 ? static_cast(lastModTime >> 8) : msb(m_entry->crc32); - - for (auto remaining = extraFieldLength; remaining;) - { - // read extra field header - const auto id = read(*m_is); - auto size = read(*m_is); - - if (remaining < 4 + size) - throw Error{"could not read central directory header"}; - remaining -= 4 + size; - - switch (id) - { - case 0x0001: // Zip64 extended information - if (8 <= size && m_entry->uncompressedSize == mask<0, 32>) - { - m_entry->uncompressedSize = read(*m_is); - size -= 8; - } - if (8 <= size && m_entry->packedSize == mask<0, 32>) - { - m_entry->packedSize = read(*m_is); - size -= 8; - } - if (8 <= size && m_entry->offset == mask<0, 32>) - { - m_entry->offset = read(*m_is); - size -= 8; - } - break; - - case 0x7075: // Info-ZIP Unicode Path - if (5 <= size) - { - const auto nameCrc32 = - std::accumulate(m_entry->name.begin(), m_entry->name.end(), mask<0, 32>, Crc32Tab::crc32) ^ - mask<0, 32>; - - m_is->seekg(1, std::ios::cur); - const auto expectedNameCrc32 = read(*m_is); - size -= 5; - - if (nameCrc32 == expectedNameCrc32) - { - m_entry->name = read(*m_is, size); - size = 0; - } - } - break; - - case 0x9901: // AE-x encryption structure - if (7 <= size) - { - m_is->seekg(5, std::ios::cur); - const auto actualMethod = read(*m_is); - size -= 7; - - m_entry->compression = static_cast(actualMethod); - } - break; - - default: - break; - } - - // jump to the end of this data block - m_is->seekg(size, std::ios::cur); - } - - m_is->seekg(fileCommentLength, std::ios::cur); - - if (!*m_is) - throw Error{"could not read central directory header"}; + const auto header = CentralDirectoryHeader::read(*m_is); + + const auto* aesExtraData = header.extraField.find(); + const auto* unicodePath = header.extraField.find(); + const auto* zip64 = header.extraField.find(); + + m_entry->name = + (unicodePath && (std::accumulate(header.filename.begin(), header.filename.end(), mask<0, 32>, Crc32Tab::crc32) ^ + mask<0, 32>) == unicodePath->nameCrc32) + ? unicodePath->unicodeName + : header.filename; + m_entry->encryption = header.getEncryption(); + m_entry->compression = static_cast(aesExtraData ? aesExtraData->method : header.method); + m_entry->crc32 = header.crc32; + m_entry->offset = zip64 && zip64->headerOffset ? *zip64->headerOffset : header.headerOffset; + m_entry->packedSize = zip64 && zip64->compressedSize ? *zip64->compressedSize : header.compressedSize; + m_entry->uncompressedSize = zip64 && zip64->uncompressedSize ? *zip64->uncompressedSize : header.uncompressedSize; + m_entry->checkByte = (header.flags >> 3) & 1 ? lsb(header.lastModTime >> 8) : msb(header.crc32); return *this; } @@ -295,8 +599,8 @@ auto Zip::seek(const Entry& entry) const -> std::istream& // skip local file header m_is.seekg(22, std::ios::cur); - const auto nameSize = read(m_is); - const auto extraSize = read(m_is); + const auto nameSize = readInt(m_is); + const auto extraSize = readInt(m_is); m_is.seekg(nameSize + extraSize, std::ios::cur); return m_is; @@ -335,15 +639,15 @@ void Zip::changeKeys(std::ostream& os, const Keys& oldKeys, const Keys& newKeys, if (!checkSignature(m_is, Signature::LocalFileHeader)) throw Error{"could not find local file header"}; - write(os, static_cast(Signature::LocalFileHeader)); + writeInt(os, static_cast(Signature::LocalFileHeader)); std::copy_n(std::istreambuf_iterator{m_is}, 22, std::ostreambuf_iterator{os}); m_is.get(); - const auto filenameLength = read(m_is); - const auto extraSize = read(m_is); - write(os, filenameLength); - write(os, extraSize); + const auto filenameLength = readInt(m_is); + const auto extraSize = readInt(m_is); + writeInt(os, filenameLength); + writeInt(os, extraSize); if (0 < filenameLength + extraSize) { @@ -372,6 +676,177 @@ void Zip::changeKeys(std::ostream& os, const Keys& oldKeys, const Keys& newKeys, std::copy(std::istreambuf_iterator{m_is}, {}, std::ostreambuf_iterator{os}); } +void Zip::decrypt(std::ostream& os, const Keys& keys, Progress& progress) const +{ + // Store encrypted entries local file header offset and packed size. + // Use std::map to sort them by local file header offset. + auto packedSizeByLocalOffset = std::map{}; + for (const auto& entry : *this) + if (entry.encryption == Encryption::Traditional) + packedSizeByLocalOffset.insert({entry.offset, entry.packedSize}); + + // Rewind input stream and iterate on encrypted entries to decipher data and update related metadata. + m_is.seekg(0, std::ios::beg); + auto currentOffset = std::uint64_t{}; + + progress.done = 0; + progress.total = packedSizeByLocalOffset.size(); + + for (const auto& [localHeaderOffset, packedSize] : packedSizeByLocalOffset) + { + if (currentOffset < localHeaderOffset) + { + std::copy_n(std::istreambuf_iterator{m_is}, localHeaderOffset - currentOffset, + std::ostreambuf_iterator{os}); + m_is.get(); + } + + // transform file header + if (!checkSignature(m_is, Signature::LocalFileHeader)) + throw Error{"could not find local file header"}; + writeInt(os, static_cast(Signature::LocalFileHeader)); + + auto fileHeader = LocalFileHeader::read(m_is); + auto* zip64ExtraData = fileHeader.extraField.find(); + fileHeader.flags &= ~1; + if (zip64ExtraData && zip64ExtraData->compressedSize) + *zip64ExtraData->compressedSize -= Data::encryptionHeaderSize; + else if (Data::encryptionHeaderSize <= fileHeader.compressedSize) + fileHeader.compressedSize -= Data::encryptionHeaderSize; + fileHeader.write(os); + + // decipher file data + decipher(m_is, packedSize, Data::encryptionHeaderSize, os, keys); + + // transform data descriptor + auto dataDescriptorSize = 0; + if ((fileHeader.flags >> 3) & 1) + { + // optional signature + crc-32 + const auto crc32OrSignature = readInt(m_is); + writeInt(os, crc32OrSignature); + dataDescriptorSize += 4; + + if (crc32OrSignature == static_cast(Signature::DataDescriptor)) + { + const auto actualCrc32 = readInt(m_is); + writeInt(os, actualCrc32); + dataDescriptorSize += 4; + } + + // compressed size, uncompressed size + if (zip64ExtraData) + { + writeInt(os, readInt(m_is) - Data::encryptionHeaderSize); + writeInt(os, readInt(m_is)); + dataDescriptorSize += 8 + 8; + } + else + { + writeInt(os, readInt(m_is) - Data::encryptionHeaderSize); + writeInt(os, readInt(m_is)); + dataDescriptorSize += 4 + 4; + } + } + + currentOffset = localHeaderOffset + 30 + fileHeader.filenameLength + fileHeader.extraFieldLength + packedSize + + dataDescriptorSize; + + progress.done++; + } + + if (currentOffset < m_centralDirectoryOffset) + { + std::copy_n(std::istreambuf_iterator{m_is}, m_centralDirectoryOffset - currentOffset, + std::ostreambuf_iterator{os}); + m_is.get(); + currentOffset = m_centralDirectoryOffset; + } + + const auto translateOffset = [&packedSizeByLocalOffset](auto offset) + { + return offset - std::distance(packedSizeByLocalOffset.begin(), packedSizeByLocalOffset.lower_bound(offset)) * + Data::encryptionHeaderSize; + }; + + // update metadata in central directory + auto signature = readInt(m_is); + + while (m_is && signature == static_cast(Signature::CentralDirectoryHeader)) + { + writeInt(os, signature); + + auto centralDirectoryHeader = CentralDirectoryHeader::read(m_is); + auto* zip64ExtraData = centralDirectoryHeader.extraField.find(); + if (centralDirectoryHeader.getEncryption() == Zip::Encryption::Traditional) + { + centralDirectoryHeader.flags &= ~1; + if (zip64ExtraData && zip64ExtraData->compressedSize) + *zip64ExtraData->compressedSize -= Data::encryptionHeaderSize; + else if (Data::encryptionHeaderSize <= centralDirectoryHeader.compressedSize) + centralDirectoryHeader.compressedSize -= Data::encryptionHeaderSize; + } + if (zip64ExtraData && zip64ExtraData->headerOffset) + *zip64ExtraData->headerOffset = translateOffset(*zip64ExtraData->headerOffset); + else + centralDirectoryHeader.headerOffset = translateOffset(centralDirectoryHeader.headerOffset); + centralDirectoryHeader.write(os); + + signature = readInt(m_is); + } + + auto isZip64 = false; + if (m_is && signature == static_cast(Signature::Zip64Eocd)) + { + writeInt(os, signature); + isZip64 = true; + + const auto sizeOfZip64Eocd = readInt(m_is); + writeInt(os, sizeOfZip64Eocd); + + std::copy_n(std::istreambuf_iterator{m_is}, 36, std::ostreambuf_iterator{os}); + m_is.get(); + + const auto eocdStartOffset = readInt(m_is); + writeInt(os, translateOffset(eocdStartOffset)); + + if (44 < sizeOfZip64Eocd) + { + std::copy_n(std::istreambuf_iterator{m_is}, sizeOfZip64Eocd - 44, std::ostreambuf_iterator{os}); + m_is.get(); + } + + if (!checkSignature(m_is, Signature::Zip64EocdLocator)) + throw Error{"could not find Zip64 end of central directory locator"}; + writeInt(os, static_cast(Signature::Zip64EocdLocator)); + + std::copy_n(std::istreambuf_iterator{m_is}, 4, std::ostreambuf_iterator{os}); + m_is.get(); + + const auto zip64EocdStartOffset = readInt(m_is); + writeInt(os, translateOffset(zip64EocdStartOffset)); + + std::copy_n(std::istreambuf_iterator{m_is}, 4, std::ostreambuf_iterator{os}); + m_is.get(); + + signature = readInt(m_is); + } + + if (!m_is || signature != static_cast(Signature::Eocd)) + throw Error{"could not find end of central directory record"}; + writeInt(os, static_cast(Signature::Eocd)); + + std::copy_n(std::istreambuf_iterator{m_is}, 12, std::ostreambuf_iterator{os}); + m_is.get(); + + auto eocdOffset = readInt(m_is); + if (!isZip64 || eocdOffset != mask<0, 32>) + eocdOffset = translateOffset(eocdOffset); + writeInt(os, eocdOffset); + + std::copy(std::istreambuf_iterator{m_is}, {}, std::ostreambuf_iterator{os}); +} + void decipher(std::istream& is, std::size_t size, std::size_t discard, std::ostream& os, Keys keys) { auto cipher = std::istreambuf_iterator{is}; diff --git a/src/main.cpp b/src/main.cpp index e9bb97e..6671976 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -55,6 +55,10 @@ Options to use the internal password representation: --keep-header Write the encryption header at the beginning of deciphered data instead of discarding it + -D, --decrypt + Create a copy of the encrypted zip archive with deciphered entries, + removing the password protection (requires -C) + -U, --change-password Create a copy of the encrypted zip archive with the password set to the given new password (requires -C) @@ -236,6 +240,18 @@ try std::cout << "Wrote deciphered data." << std::endl; } + // decrypt + if (args.decryptedArchive) + { + std::cout << "[" << put_time << "] Writing decrypted archive " << *args.decryptedArchive << std::endl; + + auto encrypted = Zip{*args.cipherArchive}; + auto decrypted = openOutput(*args.decryptedArchive); + + auto progress = ConsoleProgress{std::cout}; + encrypted.decrypt(decrypted, keys, progress); + } + // unlock if (args.changePassword) {