Fixed export of Client Config with default entcryption and end-zipping

This commit is contained in:
anhefti 2020-04-07 17:43:45 +02:00
parent a8082471bc
commit 4dcbe3793a
5 changed files with 295 additions and 29 deletions

View file

@ -0,0 +1,58 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl;
import java.security.GeneralSecurityException;
import java.security.SecureRandom;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;
import org.cryptonode.jncryptor.AES256JNCryptor;
import org.cryptonode.jncryptor.CryptorException;
class AES256JNCryptorEmptyPwdSupport extends AES256JNCryptor {
static final String KEY_DERIVATION_ALGORITHM = "PBKDF2WithHmacSHA1";
static final int AES_256_KEY_SIZE = 256 / 8;
static final String AES_NAME = "AES";
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
protected AES256JNCryptorEmptyPwdSupport() {
super();
}
protected AES256JNCryptorEmptyPwdSupport(final int iterations) {
super(iterations);
}
static byte[] getSecureRandomData(final int length) {
final byte[] result = new byte[length];
SECURE_RANDOM.nextBytes(result);
return result;
}
@Override
public SecretKey keyForPassword(final char[] password, final byte[] salt) throws CryptorException {
try {
final SecretKeyFactory factory = SecretKeyFactory
.getInstance(KEY_DERIVATION_ALGORITHM);
final SecretKey tmp = factory.generateSecret(new PBEKeySpec(password, salt,
getPBKDFIterations(), AES_256_KEY_SIZE * 8));
return new SecretKeySpec(tmp.getEncoded(), AES_NAME);
} catch (final GeneralSecurityException e) {
throw new CryptorException(String.format(
"Failed to generate key from password using %s.",
KEY_DERIVATION_ALGORITHM), e);
}
}
}

View file

@ -0,0 +1,202 @@
/*
* Copyright (c) 2020 ETH Zürich, Educational Development and Technology (LET)
*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl;
import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import org.apache.commons.lang3.Validate;
import org.cryptonode.jncryptor.CryptorException;
class AES256JNCryptorOutputStreamEmptyPwdSupport extends OutputStream {
static final int SALT_LENGTH = 8;
static final int AES_BLOCK_SIZE = 16;
static final String AES_CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding";
static final String HMAC_ALGORITHM = "HmacSHA256";
static final int VERSION = 3;
static final int FLAG_PASSWORD = 0x01;
private CipherOutputStream cipherStream;
private MacOutputStream macOutputStream;
private boolean writtenHeader;
private final boolean passwordBased;
private final byte[] encryptionSalt;
private byte[] iv;
private final byte[] hmacSalt;
/** Creates an output stream for password-encrypted data, using a specific
* number of PBKDF iterations.
*
* @param out
* the {@code OutputStream} to write the JNCryptor data to
* @param password
* the password
* @param iterations
* the number of PBKDF iterations to perform */
public AES256JNCryptorOutputStreamEmptyPwdSupport(final OutputStream out, final char[] password,
final int iterations) throws CryptorException {
Validate.notNull(out, "Output stream cannot be null.");
Validate.notNull(password, "Password cannot be null.");
Validate.isTrue(iterations > 0, "Iterations must be greater than zero.");
final AES256JNCryptorEmptyPwdSupport cryptor = new AES256JNCryptorEmptyPwdSupport(iterations);
this.encryptionSalt = AES256JNCryptorEmptyPwdSupport.getSecureRandomData(SALT_LENGTH);
final SecretKey encryptionKey = cryptor.keyForPassword(password, this.encryptionSalt);
this.hmacSalt = AES256JNCryptorEmptyPwdSupport.getSecureRandomData(SALT_LENGTH);
final SecretKey hmacKey = cryptor.keyForPassword(password, this.hmacSalt);
this.iv = AES256JNCryptorEmptyPwdSupport.getSecureRandomData(AES_BLOCK_SIZE);
this.passwordBased = true;
createStreams(encryptionKey, hmacKey, this.iv, out);
}
/** Creates the cipher and MAC streams required,
*
* @param encryptionKey
* the encryption key
* @param hmacKey
* the HMAC key
* @param iv
* the IV
* @param out
* the output stream we are wrapping
* @throws CryptorException */
private void createStreams(final SecretKey encryptionKey, final SecretKey hmacKey,
final byte[] iv, final OutputStream out) throws CryptorException {
this.iv = iv;
try {
final Cipher cipher = Cipher.getInstance(AES_CIPHER_ALGORITHM);
cipher.init(Cipher.ENCRYPT_MODE, encryptionKey, new IvParameterSpec(iv));
try {
final Mac mac = Mac.getInstance(HMAC_ALGORITHM);
mac.init(hmacKey);
this.macOutputStream = new MacOutputStream(out, mac);
this.cipherStream = new CipherOutputStream(this.macOutputStream, cipher);
} catch (final GeneralSecurityException e) {
throw new CryptorException("Failed to initialize HMac", e);
}
} catch (final GeneralSecurityException e) {
throw new CryptorException("Failed to initialize AES cipher", e);
}
}
/** Writes the header data to the output stream.
*
* @throws IOException */
private void writeHeader() throws IOException {
/* Write out the header */
if (this.passwordBased) {
this.macOutputStream.write(VERSION);
this.macOutputStream.write(FLAG_PASSWORD);
this.macOutputStream.write(this.encryptionSalt);
this.macOutputStream.write(this.hmacSalt);
this.macOutputStream.write(this.iv);
} else {
this.macOutputStream.write(VERSION);
this.macOutputStream.write(0);
this.macOutputStream.write(this.iv);
}
}
/** Writes one byte to the encrypted output stream.
*
* @param b
* the byte to write
* @throws IOException
* if an I/O error occurs */
@Override
public void write(final int b) throws IOException {
if (!this.writtenHeader) {
writeHeader();
this.writtenHeader = true;
}
this.cipherStream.write(b);
}
/** Writes bytes to the encrypted output stream.
*
* @param b
* a buffer of bytes to write
* @param off
* the offset into the buffer
* @param len
* the number of bytes to write (starting from the offset)
* @throws IOException
* if an I/O error occurs */
@Override
public void write(final byte[] b, final int off, final int len) throws IOException {
if (!this.writtenHeader) {
writeHeader();
this.writtenHeader = true;
}
this.cipherStream.write(b, off, len);
}
/** Closes the stream. This causes the HMAC calculation to be concluded and
* written to the output.
*
* @throws IOException
* if an I/O error occurs */
@Override
public void close() throws IOException {
this.cipherStream.close();
}
/** An output stream to update a Mac object with all bytes passed through, then
* write the Mac data to the stream upon close to complete the RNCryptor file
* format. */
private static class MacOutputStream extends FilterOutputStream {
private final Mac mac;
MacOutputStream(final OutputStream out, final Mac mac) {
super(out);
this.mac = mac;
}
@Override
public void write(final int b) throws IOException {
this.mac.update((byte) b);
this.out.write(b);
}
@Override
public void write(final byte[] b, final int off, final int len) throws IOException {
this.mac.update(b, off, len);
this.out.write(b, off, len);
}
@Override
public void close() throws IOException {
final byte[] macData = this.mac.doFinal();
this.out.write(macData);
this.out.flush();
this.out.close();
}
}
}

View file

@ -8,7 +8,6 @@
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
@ -194,16 +193,17 @@ public class ClientConfigServiceImpl implements ClientConfigService {
final CharSequence encryptionPassword = this.sebClientConfigDAO
.getConfigPasswordCipher(config.getModelId())
.getOr(null);
.getOr(StringUtils.EMPTY);
final String plainTextXMLContent = extractXMLContent(config);
PipedOutputStream pOut = null;
PipedInputStream pIn = null;
PipedOutputStream zipOut = null;
PipedInputStream zipIn = null;
try {
// zip the plain text
final InputStream plainIn = IOUtils.toInputStream(
Constants.XML_VERSION_HEADER +
Constants.XML_DOCTYPE_HEADER +
@ -215,37 +215,36 @@ public class ClientConfigServiceImpl implements ClientConfigService {
pOut = new PipedOutputStream();
pIn = new PipedInputStream(pOut);
zipOut = new PipedOutputStream();
zipIn = new PipedInputStream(zipOut);
// ZIP plain text
this.zipService.write(pOut, plainIn);
if (encryptionPassword != null) {
passwordEncryption(output, encryptionPassword, pIn);
// encrypt zipped plain text and add header
passwordEncryption(zipOut, encryptionPassword, pIn);
} else {
// just add plain text header
this.sebConfigEncryptionService.streamEncrypted(
output,
zipOut,
pIn,
EncryptionContext.contextOfPlainText());
}
// ZIP again to finish up
this.zipService.write(output, zipIn);
if (log.isDebugEnabled()) {
log.debug("*** Finished Seb client configuration download streaming composition");
}
} catch (final Exception e) {
log.error("Error while zip and encrypt seb client config stream: ", e);
try {
if (pIn != null) {
pIn.close();
}
} catch (final IOException e1) {
log.error("Failed to close PipedInputStream: ", e1);
}
try {
if (pOut != null) {
pOut.close();
}
} catch (final IOException e1) {
log.error("Failed to close PipedOutputStream: ", e1);
}
IOUtils.closeQuietly(pIn);
IOUtils.closeQuietly(pOut);
IOUtils.closeQuietly(zipIn);
IOUtils.closeQuietly(zipOut);
}
}
@ -382,14 +381,15 @@ public class ClientConfigServiceImpl implements ClientConfigService {
log.debug("*** Seb client configuration with password based encryption");
}
final CharSequence encryptionPasswordPlaintext = this.clientCredentialService
.decrypt(encryptionPassword);
final CharSequence encryptionPasswordPlaintext = (encryptionPassword == StringUtils.EMPTY)
? StringUtils.EMPTY
: this.clientCredentialService.decrypt(encryptionPassword);
this.sebConfigEncryptionService.streamEncrypted(
output,
input,
EncryptionContext.contextOf(
Strategy.PASSWORD_PSWD,
(encryptionPassword == StringUtils.EMPTY) ? Strategy.PASSWORD_PWCC : Strategy.PASSWORD_PSWD,
encryptionPasswordPlaintext));
}

View file

@ -65,13 +65,21 @@ public class PasswordEncryptor implements SebConfigCryptor {
log.debug("*** Start streaming asynchronous encryption");
}
AES256JNCryptorOutputStream encryptOutput = null;
OutputStream encryptOutput = null;
try {
encryptOutput = new AES256JNCryptorOutputStream(
output,
Utils.toCharArray(context.getPassword()),
Constants.JN_CRYPTOR_ITERATIONS);
final CharSequence password = context.getPassword();
if (password.length() == 0) {
encryptOutput = new AES256JNCryptorOutputStreamEmptyPwdSupport(
output,
Utils.toCharArray(password),
Constants.JN_CRYPTOR_ITERATIONS);
} else {
encryptOutput = new AES256JNCryptorOutputStream(
output,
Utils.toCharArray(password),
Constants.JN_CRYPTOR_ITERATIONS);
}
IOUtils.copyLarge(input, encryptOutput);

View file

@ -1000,7 +1000,6 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
List<String> readLines = IOUtils.readLines(exportResponse.get(), "UTF-8");
assertNotNull(readLines);
assertFalse(readLines.isEmpty());
assertTrue(readLines.get(0).startsWith("plnd"));
// export client config With Password Protection
exportResponse = restService
@ -1014,7 +1013,6 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
readLines = IOUtils.readLines(exportResponse.get(), "UTF-8");
assertNotNull(readLines);
assertFalse(readLines.isEmpty());
assertTrue(readLines.get(0).startsWith("pswd"));
// get page
final Result<Page<SebClientConfig>> pageResponse = restService