diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/AES256JNCryptorEmptyPwdSupport.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/AES256JNCryptorEmptyPwdSupport.java new file mode 100644 index 00000000..a1e3643c --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/AES256JNCryptorEmptyPwdSupport.java @@ -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); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/AES256JNCryptorOutputStreamEmptyPwdSupport.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/AES256JNCryptorOutputStreamEmptyPwdSupport.java new file mode 100644 index 00000000..dfb453d4 --- /dev/null +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/AES256JNCryptorOutputStreamEmptyPwdSupport.java @@ -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(); + } + } + +} diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ClientConfigServiceImpl.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ClientConfigServiceImpl.java index 68850e91..be8ebe4a 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ClientConfigServiceImpl.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/ClientConfigServiceImpl.java @@ -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)); } diff --git a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/PasswordEncryptor.java b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/PasswordEncryptor.java index df7d76a7..2095b0f7 100644 --- a/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/PasswordEncryptor.java +++ b/src/main/java/ch/ethz/seb/sebserver/webservice/servicelayer/sebconfig/impl/PasswordEncryptor.java @@ -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); diff --git a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java index e552fe3a..887c1910 100644 --- a/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java +++ b/src/test/java/ch/ethz/seb/sebserver/gui/integration/UseCasesIntegrationTest.java @@ -1000,7 +1000,6 @@ public class UseCasesIntegrationTest extends GuiIntegrationTest { List 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> pageResponse = restService