Fixed export of Client Config with default entcryption and end-zipping
This commit is contained in:
parent
a8082471bc
commit
4dcbe3793a
5 changed files with 295 additions and 29 deletions
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl;
|
package ch.ethz.seb.sebserver.webservice.servicelayer.sebconfig.impl;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.io.InputStream;
|
import java.io.InputStream;
|
||||||
import java.io.OutputStream;
|
import java.io.OutputStream;
|
||||||
import java.io.PipedInputStream;
|
import java.io.PipedInputStream;
|
||||||
|
@ -194,16 +193,17 @@ public class ClientConfigServiceImpl implements ClientConfigService {
|
||||||
|
|
||||||
final CharSequence encryptionPassword = this.sebClientConfigDAO
|
final CharSequence encryptionPassword = this.sebClientConfigDAO
|
||||||
.getConfigPasswordCipher(config.getModelId())
|
.getConfigPasswordCipher(config.getModelId())
|
||||||
.getOr(null);
|
.getOr(StringUtils.EMPTY);
|
||||||
|
|
||||||
final String plainTextXMLContent = extractXMLContent(config);
|
final String plainTextXMLContent = extractXMLContent(config);
|
||||||
|
|
||||||
PipedOutputStream pOut = null;
|
PipedOutputStream pOut = null;
|
||||||
PipedInputStream pIn = null;
|
PipedInputStream pIn = null;
|
||||||
|
PipedOutputStream zipOut = null;
|
||||||
|
PipedInputStream zipIn = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
|
||||||
// zip the plain text
|
|
||||||
final InputStream plainIn = IOUtils.toInputStream(
|
final InputStream plainIn = IOUtils.toInputStream(
|
||||||
Constants.XML_VERSION_HEADER +
|
Constants.XML_VERSION_HEADER +
|
||||||
Constants.XML_DOCTYPE_HEADER +
|
Constants.XML_DOCTYPE_HEADER +
|
||||||
|
@ -215,37 +215,36 @@ public class ClientConfigServiceImpl implements ClientConfigService {
|
||||||
pOut = new PipedOutputStream();
|
pOut = new PipedOutputStream();
|
||||||
pIn = new PipedInputStream(pOut);
|
pIn = new PipedInputStream(pOut);
|
||||||
|
|
||||||
|
zipOut = new PipedOutputStream();
|
||||||
|
zipIn = new PipedInputStream(zipOut);
|
||||||
|
|
||||||
|
// ZIP plain text
|
||||||
this.zipService.write(pOut, plainIn);
|
this.zipService.write(pOut, plainIn);
|
||||||
|
|
||||||
if (encryptionPassword != null) {
|
if (encryptionPassword != null) {
|
||||||
passwordEncryption(output, encryptionPassword, pIn);
|
// encrypt zipped plain text and add header
|
||||||
|
passwordEncryption(zipOut, encryptionPassword, pIn);
|
||||||
} else {
|
} else {
|
||||||
|
// just add plain text header
|
||||||
this.sebConfigEncryptionService.streamEncrypted(
|
this.sebConfigEncryptionService.streamEncrypted(
|
||||||
output,
|
zipOut,
|
||||||
pIn,
|
pIn,
|
||||||
EncryptionContext.contextOfPlainText());
|
EncryptionContext.contextOfPlainText());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ZIP again to finish up
|
||||||
|
this.zipService.write(output, zipIn);
|
||||||
|
|
||||||
if (log.isDebugEnabled()) {
|
if (log.isDebugEnabled()) {
|
||||||
log.debug("*** Finished Seb client configuration download streaming composition");
|
log.debug("*** Finished Seb client configuration download streaming composition");
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (final Exception e) {
|
} catch (final Exception e) {
|
||||||
log.error("Error while zip and encrypt seb client config stream: ", e);
|
log.error("Error while zip and encrypt seb client config stream: ", e);
|
||||||
try {
|
IOUtils.closeQuietly(pIn);
|
||||||
if (pIn != null) {
|
IOUtils.closeQuietly(pOut);
|
||||||
pIn.close();
|
IOUtils.closeQuietly(zipIn);
|
||||||
}
|
IOUtils.closeQuietly(zipOut);
|
||||||
} 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -382,14 +381,15 @@ public class ClientConfigServiceImpl implements ClientConfigService {
|
||||||
log.debug("*** Seb client configuration with password based encryption");
|
log.debug("*** Seb client configuration with password based encryption");
|
||||||
}
|
}
|
||||||
|
|
||||||
final CharSequence encryptionPasswordPlaintext = this.clientCredentialService
|
final CharSequence encryptionPasswordPlaintext = (encryptionPassword == StringUtils.EMPTY)
|
||||||
.decrypt(encryptionPassword);
|
? StringUtils.EMPTY
|
||||||
|
: this.clientCredentialService.decrypt(encryptionPassword);
|
||||||
|
|
||||||
this.sebConfigEncryptionService.streamEncrypted(
|
this.sebConfigEncryptionService.streamEncrypted(
|
||||||
output,
|
output,
|
||||||
input,
|
input,
|
||||||
EncryptionContext.contextOf(
|
EncryptionContext.contextOf(
|
||||||
Strategy.PASSWORD_PSWD,
|
(encryptionPassword == StringUtils.EMPTY) ? Strategy.PASSWORD_PWCC : Strategy.PASSWORD_PSWD,
|
||||||
encryptionPasswordPlaintext));
|
encryptionPasswordPlaintext));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -65,13 +65,21 @@ public class PasswordEncryptor implements SebConfigCryptor {
|
||||||
log.debug("*** Start streaming asynchronous encryption");
|
log.debug("*** Start streaming asynchronous encryption");
|
||||||
}
|
}
|
||||||
|
|
||||||
AES256JNCryptorOutputStream encryptOutput = null;
|
OutputStream encryptOutput = null;
|
||||||
try {
|
try {
|
||||||
|
|
||||||
encryptOutput = new AES256JNCryptorOutputStream(
|
final CharSequence password = context.getPassword();
|
||||||
output,
|
if (password.length() == 0) {
|
||||||
Utils.toCharArray(context.getPassword()),
|
encryptOutput = new AES256JNCryptorOutputStreamEmptyPwdSupport(
|
||||||
Constants.JN_CRYPTOR_ITERATIONS);
|
output,
|
||||||
|
Utils.toCharArray(password),
|
||||||
|
Constants.JN_CRYPTOR_ITERATIONS);
|
||||||
|
} else {
|
||||||
|
encryptOutput = new AES256JNCryptorOutputStream(
|
||||||
|
output,
|
||||||
|
Utils.toCharArray(password),
|
||||||
|
Constants.JN_CRYPTOR_ITERATIONS);
|
||||||
|
}
|
||||||
|
|
||||||
IOUtils.copyLarge(input, encryptOutput);
|
IOUtils.copyLarge(input, encryptOutput);
|
||||||
|
|
||||||
|
|
|
@ -1000,7 +1000,6 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
|
||||||
List<String> readLines = IOUtils.readLines(exportResponse.get(), "UTF-8");
|
List<String> readLines = IOUtils.readLines(exportResponse.get(), "UTF-8");
|
||||||
assertNotNull(readLines);
|
assertNotNull(readLines);
|
||||||
assertFalse(readLines.isEmpty());
|
assertFalse(readLines.isEmpty());
|
||||||
assertTrue(readLines.get(0).startsWith("plnd"));
|
|
||||||
|
|
||||||
// export client config With Password Protection
|
// export client config With Password Protection
|
||||||
exportResponse = restService
|
exportResponse = restService
|
||||||
|
@ -1014,7 +1013,6 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest {
|
||||||
readLines = IOUtils.readLines(exportResponse.get(), "UTF-8");
|
readLines = IOUtils.readLines(exportResponse.get(), "UTF-8");
|
||||||
assertNotNull(readLines);
|
assertNotNull(readLines);
|
||||||
assertFalse(readLines.isEmpty());
|
assertFalse(readLines.isEmpty());
|
||||||
assertTrue(readLines.get(0).startsWith("pswd"));
|
|
||||||
|
|
||||||
// get page
|
// get page
|
||||||
final Result<Page<SebClientConfig>> pageResponse = restService
|
final Result<Page<SebClientConfig>> pageResponse = restService
|
||||||
|
|
Loading…
Reference in a new issue