com.metaeffekt.artifact.analysis.flow.ng.crypt.EncryptedZipProvider Maven / Gradle / Ivy
/*
* Copyright 2021-2024 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.metaeffekt.artifact.analysis.flow.ng.crypt;
import com.fasterxml.jackson.databind.MappingIterator;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.metaeffekt.artifact.analysis.flow.ng.DecryptableKeyslot;
import com.metaeffekt.artifact.analysis.flow.ng.crypt.param.ProviderParameters;
import com.metaeffekt.artifact.analysis.flow.ng.exception.DecryptionImpossibleException;
import com.metaeffekt.artifact.analysis.flow.ng.exception.SelfcheckFailedException;
import com.metaeffekt.artifact.analysis.flow.ng.keyholder.UserKeysForConsumer;
import com.metaeffekt.artifact.analysis.flow.ng.keyholder.UserKeysStorage;
import org.apache.commons.io.IOUtils;
import org.bouncycastle.crypto.InvalidCipherTextException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.crypto.AEADBadTagException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.StandardOpenOption;
import java.security.*;
import java.util.zip.ZipInputStream;
import static com.metaeffekt.artifact.analysis.flow.ng.EncryptedArchiveConstants.KEYSLOT_ENTRY_NAME;
import static com.metaeffekt.artifact.analysis.flow.ng.StreamReadingUtils.skipToEntry;
/**
* Assists in reading encrypted archives, hiding low-level annoyances.
*/
public class EncryptedZipProvider {
private static final Logger LOG = LoggerFactory.getLogger(EncryptedZipProvider.class);
private final ProviderParameters param;
public EncryptedZipProvider(ProviderParameters param) {
this.param = param;
}
/**
* Reads keyslots from the stream to find a content key.
*
* This method may close the stream once it's done.
* @param keyslotsFileInputStream the input stream to read keyslots from
* @return returns the found content key (or throws otherwise)
* @throws IOException throws if reading failed
* @throws InvalidCipherTextException throws if decryption of the content key failed
*/
private Key getDecryptedContentKey(InputStream keyslotsFileInputStream) throws IOException,
InvalidCipherTextException {
// read user keys
UserKeysForConsumer userKeys;
// try to read the keyfile twice (once with selfcheck to warn the user of integrity errors, then try anyway)
try {
userKeys = UserKeysStorage.readUserKeysForConsumer(param.getUserKeysFile(), param.getPassword(), true);
} catch (SelfcheckFailedException e) {
LOG.warn("Keyfile's selfcheck failed: possibly corrupt at [{}]", param.getUserKeysFile().getPath());
userKeys = UserKeysStorage.readUserKeysForConsumer(param.getUserKeysFile(), param.getPassword(), false);
}
// read keyslots, trying to find the correct one
try (final BufferedInputStream bufferedInputStream = new BufferedInputStream(keyslotsFileInputStream, 16384);
final Reader reader = new InputStreamReader(bufferedInputStream, StandardCharsets.UTF_8);
final MappingIterator mappingIterator = new ObjectMapper()
.readerFor(DecryptableKeyslot.class)
.readValues(reader) ) {
while (mappingIterator.hasNext()) {
DecryptableKeyslot toCheck = mappingIterator.next();
if (toCheck.checkHmac(userKeys)) {
// decrypt keyslot getting content key
byte[] contentKey = toCheck.getContentKeyUsing(userKeys);
return new SecretKeySpec(contentKey,
param.getContentAlgorithmParam().getAlgorithm()
+ param.getContentAlgorithmParam().getMode());
}
}
}
return null;
}
/**
* Processes the keyslots file, taken from ProviderParameters.
* @param param parameters for processing
* @return returns the content key (if found).
* @throws IOException throws on io errors
* @throws InvalidCipherTextException throws if a used cipher is not supported by the environment
*/
private Key processKeyslotsFile(ProviderParameters param) throws IOException, InvalidCipherTextException {
// iterate key slots, trying to find the correct one for decryption
try (InputStream inputStream = Files.newInputStream(param.getEncryptedZipPackage().toPath());
ZipInputStream zipInputStream = new ZipInputStream(inputStream)) {
// err if we can't find the entry
if (!skipToEntry(zipInputStream, KEYSLOT_ENTRY_NAME)) {
throw new RuntimeException("Zip package incompatible: could not find keyslots file");
}
// should now have selected the keyslots entry, stream should be ready
return getDecryptedContentKey(zipInputStream);
}
}
public InputStream getEntryStream(String entryName) throws IOException, InvalidAlgorithmParameterException,
NoSuchPaddingException, NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException,
InvalidCipherTextException {
Key contentKey = processKeyslotsFile(param);
if (contentKey == null) {
throw new DecryptionImpossibleException("No content key found for the specified user key.");
}
try (final InputStream fileIn = Files.newInputStream(param.getEncryptedZipPackage().toPath(),
StandardOpenOption.READ);
final ZipInputStream zipInputStream = new ZipInputStream(fileIn)) {
if (!skipToEntry(zipInputStream, entryName)) {
throw new FileNotFoundException("Zip package incompatible: could not find requested entry");
}
DecryptedEntryInputStream.ensureAuthenticated(zipInputStream, param.getContentAlgorithmParam(), contentKey);
} catch (AEADBadTagException e) {
throw new RuntimeException(e);
}
// and then stream it
final InputStream fileIn = Files.newInputStream(param.getEncryptedZipPackage().toPath(),
StandardOpenOption.READ);
final ZipInputStream zipInputStream = new ZipInputStream(fileIn);
if (!skipToEntry(zipInputStream, entryName)) {
throw new FileNotFoundException("Zip package incompatible: could not find requested entry");
}
//return DecryptedEntryInputStream.createEntryDecryptionInputStream(zipInputStream, contentKey, true);
return new ByteArrayInputStream(readEntryCompletely(entryName));
}
/**
* Tries to find and read a zip entry and decrypt it.
* @param entryName the name of the entry to read. Usually in
* {@link com.metaeffekt.artifact.analysis.flow.ng.EncryptedArchiveConstants EncryptedArchiveConstants}.
* @return raw decrypted bytes.
* @throws IOException throws on error
* @throws InvalidCipherTextException throws on error
* @throws InvalidAlgorithmParameterException throws on error
* @throws NoSuchPaddingException throws on error
* @throws NoSuchAlgorithmException throws on error
* @throws NoSuchProviderException throws on error
* @throws InvalidKeyException throws on error
*/
public byte[] readEntryCompletely(String entryName)
throws IOException, InvalidCipherTextException, InvalidAlgorithmParameterException, NoSuchPaddingException,
NoSuchAlgorithmException, NoSuchProviderException, InvalidKeyException {
Key contentKey = processKeyslotsFile(param);
try (final InputStream fileIn = Files.newInputStream(param.getEncryptedZipPackage().toPath(),
StandardOpenOption.READ);
final ZipInputStream zipInputStream = new ZipInputStream(fileIn)) {
if (!skipToEntry(zipInputStream, entryName)) {
throw new FileNotFoundException("Zip package incompatible: could not find requested entry");
}
byte[] byteArray;
try (final DecryptedEntryInputStream decryptedInputStream =
DecryptedEntryInputStream.createEntryDecryptionInputStream(
zipInputStream,
param.getContentAlgorithmParam(),
contentKey
)) {
// decrypt the entire file and dump it to the user
// we do this in one go since we're using a streaming concept under the hood.
// this means that authentication tag mismatches could otherwise not be found until after parsing
// faulty decrypted data.
// the alternative would be to change the API to read an entry fully, authenticate and then stream it.
byteArray = IOUtils.toByteArray(decryptedInputStream);
}
return byteArray;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy