Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[COMPRESS-633] Add encryption support for SevenZ #332

Merged
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ target
.classpath
.settings
.idea
.vscode
*.iml
*~
/.externalToolBuilders/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
/*
* 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.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
* Options for {@link SevenZMethod#AES256SHA256} encoder
*
* @since 1.23
* @see AES256SHA256Decoder
*/
class AES256Options {

private final byte[] salt;
private final byte[] iv;
private final int numCyclesPower;
private final Cipher cipher;

/**
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please complete the Javadoc comments. You need a starting sentence.

* @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 = new SecretKeySpec(aesKeyBytes, "AES");

try {
cipher = Cipher.getInstance("AES/CBC/NoPadding");
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
);
}
}

byte[] getIv() {
return iv;
}

int getNumCyclesPower() {
return numCyclesPower;
}

byte[] getSalt() {
return salt;
}

Cipher getCipher() {
return cipher;
}

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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,21 +17,32 @@
*/
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;
import java.util.Arrays;

import javax.crypto.Cipher;
import javax.crypto.CipherInputStream;
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 AbstractCoder {

AES256SHA256Decoder() {
super(AES256Options.class);
}

@Override
InputStream decode(final String archiveName, final InputStream in, final long uncompressedLength,
Expand Down Expand Up @@ -73,26 +84,7 @@ private CipherInputStream init() throws IOException {
System.arraycopy(passwordBytes, 0, aesKeyBytes, saltSize,
Math.min(passwordBytes.length, aesKeyBytes.length - saltSize));
} else {
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(passwordBytes);
digest.update(extra);
for (int k = 0; k < extra.length; k++) {
++extra[k];
if (extra[k] != 0) {
break;
}
}
}
aesKeyBytes = digest.digest();
aesKeyBytes = sha256Password(passwordBytes, numCyclesPower, salt);
}

final SecretKey aesKey = new SecretKeySpec(aesKeyBytes, "AES");
Expand All @@ -103,8 +95,8 @@ private CipherInputStream init() throws IOException {
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 @@ -127,4 +119,133 @@ public void close() throws IOException {
}
};
}

@Override
OutputStream encode(OutputStream out, Object options) throws IOException {
final AES256Options opts = (AES256Options) options;

return new OutputStream() {
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
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();
}
}

@Override
public void write(byte[] b, int off, int len) throws IOException {
int gap = len + count > cipherBlockSize ? cipherBlockSize - count : len;
System.arraycopy(b, off, cipherBlockBuffer, count, gap);
count += gap;

if (count == cipherBlockSize) {
flushBuffer();

if (len - gap >= cipherBlockSize) {
// skip buffer to encrypt data chunks big enought to fit cipher block size
int multipleCipherBlockSizeLen = (len - gap) / cipherBlockSize * cipherBlockSize;
cipherOutputStream.write(b, off + gap, multipleCipherBlockSizeLen);
gap += multipleCipherBlockSizeLen;
}
System.arraycopy(b, off + gap, cipherBlockBuffer, 0, len - gap);
count = len - gap;
}
}

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);
}
cipherOutputStream.close();
}
};
}

@Override
byte[] getOptionsAsProperties(Object options) throws IOException {
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.getNumCyclesPower() | (opts.getSalt().length == 0 ? 0 : (1 << 7)) | (opts.getIv().length == 0 ? 0 : (1 << 6)));

if (opts.getSalt().length != 0 || opts.getIv().length != 0) {
// second byte : size of salt/iv data
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.getSalt(), 0, props, 2, opts.getSalt().length);
System.arraycopy(opts.getIv(), 0, props, 2 + opts.getSalt().length, opts.getIv().length);
}

return props;
}

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

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();
}

/**
* Convenience method that encodes Unicode characters into bytes in UTF-16 (ittle-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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@
import java.io.InputStream;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;
import java.nio.CharBuffer;
import java.nio.channels.Channels;
import java.nio.channels.SeekableByteChannel;
import java.nio.file.Files;
Expand Down Expand Up @@ -137,7 +136,7 @@ public SevenZFile(final File fileName, final char[] password) throws IOException
*/
public SevenZFile(final File fileName, final char[] password, final SevenZFileOptions options) throws IOException {
this(Files.newByteChannel(fileName.toPath(), EnumSet.of(StandardOpenOption.READ)), // NOSONAR
fileName.getAbsolutePath(), utf16Decode(password), true, options);
fileName.getAbsolutePath(), AES256SHA256Decoder.utf16Decode(password), true, options);
}

/**
Expand Down Expand Up @@ -256,7 +255,7 @@ public SevenZFile(final SeekableByteChannel channel, final String fileName,
*/
public SevenZFile(final SeekableByteChannel channel, final String fileName, final char[] password,
final SevenZFileOptions options) throws IOException {
this(channel, fileName, utf16Decode(password), false, options);
this(channel, fileName, AES256SHA256Decoder.utf16Decode(password), false, options);
}

/**
Expand Down Expand Up @@ -2056,19 +2055,6 @@ public String getDefaultName() {
return lastSegment + "~";
}

private 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;
}

private static int assertFitsIntoNonNegativeInt(final String what, final long value) throws IOException {
if (value > Integer.MAX_VALUE || value < 0) {
throw new IOException("Cannot handle " + what + " " + value);
Expand Down
Loading