Skip to content

Commit

Permalink
fix: update with lastest changes from common-compress`
Browse files Browse the repository at this point in the history
  • Loading branch information
Dougniel committed Dec 21, 2022
1 parent ed60a16 commit a00b4b9
Show file tree
Hide file tree
Showing 37 changed files with 8,171 additions and 2,766 deletions.
1 change: 1 addition & 0 deletions src/main/java/nc/opt/util/J7zip.java
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,7 @@ public static void decompress(String in, String destination, String password, bo
name = Paths.get(entry.getName()).toFile().getName();
}
try (OutputStream out = new FileOutputStream(new File(destination, name))) {
// FIXME: what if the file is big ? What about what is actually read ?
byte[] content = new byte[(int) entry.getSize()];
sevenZFile.read(content, 0, content.length);
out.write(content);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,109 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You 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 org.apache.commons.compress.archivers.sevenz;

import java.util.Arrays;
import java.util.Random;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;

public class AES256Options {
byte[] password;
byte[] salt = new byte[0];
byte[] iv = new byte[16];
int numCyclesPower = 19;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

public AES256Options(byte[] password) {
this.password = password;
new Random(Arrays.hashCode(password)).nextBytes(salt);
new Random(Arrays.hashCode(password)).nextBytes(iv);
/**
* Options for {@link SevenZMethod#AES256SHA256} encoder
*
* @since 1.23
* @see AES256SHA256Decoder
*/
class AES256Options {

static final String ALGORITHM = "AES";

static final String TRANSFORMATION = "AES/CBC/NoPadding";

static SecretKeySpec newSecretKeySpec(final byte[] bytes) {
return new SecretKeySpec(bytes, ALGORITHM);
}
private static byte[] randomBytes(int size) {
byte[] bytes = new byte[size];
try {
SecureRandom.getInstanceStrong().nextBytes(bytes);
} catch (NoSuchAlgorithmException e) {
throw new IllegalStateException("No strong secure random available to generate strong AES key", e);
}
return bytes;
}
private final byte[] salt;
private final byte[] iv;

private final int numCyclesPower;

private final Cipher cipher;

/**
* @param password password used for encryption
*/
public AES256Options(char[] password) {
this(password, new byte[0], randomBytes(16), 19);
}

/**
* @param password password used for encryption
* @param salt for password hash salting (enforce password security)
* @param iv Initialization Vector (IV) used by cipher algorithm
* @param numCyclesPower another password security enforcer parameter that controls the cycles of password hashing. More the
* this number is high, more security you'll have but also high CPU usage
*/
public AES256Options(char[] password, byte[] salt, byte[] iv, int numCyclesPower) {
this.salt = salt;
this.iv = iv;
this.numCyclesPower = numCyclesPower;

// NOTE: for security purposes, password is wrapped in a Cipher as soon as possible to not stay in memory
final byte[] aesKeyBytes = AES256SHA256Decoder.sha256Password(password, numCyclesPower, salt);
final SecretKey aesKey = newSecretKeySpec(aesKeyBytes);

try {
cipher = Cipher.getInstance(TRANSFORMATION);
cipher.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(iv));
} catch (final GeneralSecurityException generalSecurityException) {
throw new IllegalStateException(
"Encryption error (do you have the JCE Unlimited Strength Jurisdiction Policy Files installed?)",
generalSecurityException
);
}
}

Cipher getCipher() {
return cipher;
}

byte[] getIv() {
return iv;
}

int getNumCyclesPower() {
return numCyclesPower;
}

byte[] getSalt() {
return salt;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,13 @@
*/
package org.apache.commons.compress.archivers.sevenz;

import static java.nio.charset.StandardCharsets.UTF_16LE;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
Expand All @@ -30,11 +34,56 @@
import javax.crypto.CipherOutputStream;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import org.apache.commons.compress.PasswordRequiredException;

class AES256SHA256Decoder extends CoderBase {
class AES256SHA256Decoder extends AbstractCoder {

static byte[] sha256Password(final byte[] password, final int numCyclesPower, final byte[] salt) {
final MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-256");
} catch (final NoSuchAlgorithmException noSuchAlgorithmException) {
throw new IllegalStateException("SHA-256 is unsupported by your Java implementation", noSuchAlgorithmException);
}
final byte[] extra = new byte[8];
for (long j = 0; j < (1L << numCyclesPower); j++) {
digest.update(salt);
digest.update(password);
digest.update(extra);
for (int k = 0; k < extra.length; k++) {
++extra[k];
if (extra[k] != 0) {
break;
}
}
}
return digest.digest();
}

static byte[] sha256Password(final char[] password, final int numCyclesPower, final byte[] salt) {
return sha256Password(utf16Decode(password), numCyclesPower, salt);
}

/**
* Convenience method that encodes Unicode characters into bytes in UTF-16 (little-endian byte order) charset
*
* @param chars characters to encode
* @return encoded characters
* @since 1.23
*/
static byte[] utf16Decode(final char[] chars) {
if (chars == null) {
return null;
}
final ByteBuffer encoded = UTF_16LE.encode(CharBuffer.wrap(chars));
if (encoded.hasArray()) {
return encoded.array();
}
final byte[] e = new byte[encoded.remaining()];
encoded.get(e);
return e;
}

AES256SHA256Decoder() {
super(AES256Options.class);
Expand All @@ -47,6 +96,13 @@ InputStream decode(final String archiveName, final InputStream in, final long un
private boolean isInitialized;
private CipherInputStream cipherInputStream;

@Override
public void close() throws IOException {
if (cipherInputStream != null) {
cipherInputStream.close();
}
}

private CipherInputStream init() throws IOException {
if (isInitialized) {
return cipherInputStream;
Expand Down Expand Up @@ -83,16 +139,16 @@ private CipherInputStream init() throws IOException {
aesKeyBytes = sha256Password(passwordBytes, numCyclesPower, salt);
}

final SecretKey aesKey = new SecretKeySpec(aesKeyBytes, "AES");
final SecretKey aesKey = AES256Options.newSecretKeySpec(aesKeyBytes);
try {
final Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
final Cipher cipher = Cipher.getInstance(AES256Options.TRANSFORMATION);
cipher.init(Cipher.DECRYPT_MODE, aesKey, new IvParameterSpec(iv));
cipherInputStream = new CipherInputStream(in, cipher);
isInitialized = true;
return cipherInputStream;
} catch (final GeneralSecurityException generalSecurityException) {
throw new IOException("Decryption error " +
"(do you have the JCE Unlimited Strength Jurisdiction Policy Files installed?)",
throw new IllegalStateException(
"Decryption error (do you have the JCE Unlimited Strength Jurisdiction Policy Files installed?)",
generalSecurityException);
}
}
Expand All @@ -106,48 +162,40 @@ public int read() throws IOException {
public int read(final byte[] b, final int off, final int len) throws IOException {
return init().read(b, off, len);
}

@Override
public void close() throws IOException {
if (cipherInputStream != null) {
cipherInputStream.close();
}
}
};
}

@Override
OutputStream encode(OutputStream out, Object options) throws IOException {
AES256Options opts = (AES256Options) options;
final byte[] aesKeyBytes = sha256Password(opts.password, opts.numCyclesPower, opts.salt);
final SecretKey aesKey = new SecretKeySpec(aesKeyBytes, "AES");
final AES256Options opts = (AES256Options) options;

final Cipher cipher;
try {
cipher = Cipher.getInstance("AES/CBC/NoPadding");
cipher.init(Cipher.ENCRYPT_MODE, aesKey, new IvParameterSpec(opts.iv));
} catch (final GeneralSecurityException generalSecurityException) {
throw new IOException(
"Encryption error " + "(do you have the JCE Unlimited Strength Jurisdiction Policy Files installed?)",
generalSecurityException
);
}
return new OutputStream() {
final CipherOutputStream cipherOutputStream = new CipherOutputStream(out, cipher);
private final CipherOutputStream cipherOutputStream = new CipherOutputStream(out, opts.getCipher());

// Ensures that data are encrypt in respect of cipher block size and pad with '0' if smaller
// NOTE: As "AES/CBC/PKCS5Padding" is weak and should not be used, we use "AES/CBC/NoPadding" with this
// manual implementation for padding possible thanks to the size of the file stored separately
final int cipherBlockSize = cipher.getBlockSize();
final byte[] cipherBlockBuffer = new byte[cipherBlockSize];
private final int cipherBlockSize = opts.getCipher().getBlockSize();
private final byte[] cipherBlockBuffer = new byte[cipherBlockSize];
private int count = 0;

@Override
public void write(int b) throws IOException {
cipherBlockBuffer[count++] = (byte) b;
if (count == cipherBlockSize) {
flushBuffer();
public void close() throws IOException {
if (count > 0) {
cipherOutputStream.write(cipherBlockBuffer);
}
cipherOutputStream.close();
}

@Override
public void flush() throws IOException {
cipherOutputStream.flush();
}

private void flushBuffer() throws IOException {
cipherOutputStream.write(cipherBlockBuffer);
count = 0;
Arrays.fill(cipherBlockBuffer, (byte) 0);
}

@Override
Expand All @@ -170,64 +218,31 @@ public void write(byte[] b, int off, int len) throws IOException {
}
}

private void flushBuffer() throws IOException {
cipherOutputStream.write(cipherBlockBuffer);
count = 0;
Arrays.fill(cipherBlockBuffer, (byte) 0);
}

@Override
public void flush() throws IOException {
cipherOutputStream.flush();
}

@Override
public void close() throws IOException {
if (count > 0) {
cipherOutputStream.write(cipherBlockBuffer);
public void write(int b) throws IOException {
cipherBlockBuffer[count++] = (byte) b;
if (count == cipherBlockSize) {
flushBuffer();
}
cipherOutputStream.close();
}
};
}

private byte[] sha256Password(final byte[] password, final int numCyclesPower, final byte[] salt) throws IOException {
final MessageDigest digest;
try {
digest = MessageDigest.getInstance("SHA-256");
} catch (final NoSuchAlgorithmException noSuchAlgorithmException) {
throw new IOException("SHA-256 is unsupported by your Java implementation", noSuchAlgorithmException);
}
final byte[] extra = new byte[8];
for (long j = 0; j < (1L << numCyclesPower); j++) {
digest.update(salt);
digest.update(password);
digest.update(extra);
for (int k = 0; k < extra.length; k++) {
++extra[k];
if (extra[k] != 0) {
break;
}
}
}
return digest.digest();
}

@Override
byte[] getOptionsAsProperties(Object options) throws IOException {
AES256Options opts = (AES256Options) options;
byte[] props = new byte[2 + opts.salt.length + opts.iv.length];
final AES256Options opts = (AES256Options) options;
final byte[] props = new byte[2 + opts.getSalt().length + opts.getIv().length];

// First byte : control (numCyclesPower + flags of salt or iv presence)
props[0] = (byte) (opts.numCyclesPower | (opts.salt.length == 0 ? 0 : (1 << 7)) | (opts.iv.length == 0 ? 0 : (1 << 6)));
props[0] = (byte) (opts.getNumCyclesPower() | (opts.getSalt().length == 0 ? 0 : (1 << 7)) | (opts.getIv().length == 0 ? 0 : (1 << 6)));

if (opts.salt.length != 0 || opts.iv.length != 0) {
if (opts.getSalt().length != 0 || opts.getIv().length != 0) {
// second byte : size of salt/iv data
props[1] = (byte) (((opts.salt.length == 0 ? 0 : opts.salt.length - 1) << 4) | (opts.iv.length == 0 ? 0 : opts.iv.length - 1));
props[1] = (byte) (((opts.getSalt().length == 0 ? 0 : opts.getSalt().length - 1) << 4) | (opts.getIv().length == 0 ? 0 : opts.getIv().length - 1));

// remain bytes : salt/iv data
System.arraycopy(opts.salt, 0, props, 2, opts.salt.length);
System.arraycopy(opts.iv, 0, props, 2 + opts.salt.length, opts.iv.length);
System.arraycopy(opts.getSalt(), 0, props, 2, opts.getSalt().length);
System.arraycopy(opts.getIv(), 0, props, 2 + opts.getSalt().length, opts.getIv().length);
}

return props;
Expand Down
Loading

0 comments on commit a00b4b9

Please sign in to comment.