diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..0e08d29 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,4 @@ +FROM java:8-jdk-alpine +COPY target/coding-0.0.1-SNAPSHOT-jar-with-dependencies.jar /usr/app/ +WORKDIR /usr/app +ENTRYPOINT ["java", "-jar", "coding-0.0.1-SNAPSHOT-jar-with-dependencies.jar"] \ No newline at end of file diff --git a/README.md b/README.md index a84fba4..b27fe38 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,27 @@ -# coding -Project 4fun +# 3DES Encryption +3DES Encryption algorithm implementation in Java +- The application takes arguments from command line, the first argument is an encryption key, the second is text to be encrypted +- If encryption key and encryption text weren't passed from the command line, the application will try reading them from environment variables + `ENCRYPTION_KEY` `TEXT_TO_ENCRYPT` +- The application uses a 24 bit encryption key and 8 bit initialization vector +- Encryption key and initialization vector use SHA1PRNG for key generation +- DESede/CBC/PKCS5Padding is used as transformation +- Null values are not encrypted, if null used as an input for the encrypt method, the method will immediately return the null reference. The same +applies to the decrypt method + +## To run the application +- Checkout the project +- `mvn package` +- `java -jar coding-0.0.1-SNAPSHOT-jar-with-dependencies.jar KEY 'text to encrypt'` + +## To run the application via docker-compose +- Checkout the project +- `mvn package` +- Open the `docker-compose.yml` and set `ENCRYPTION_KEY` `TEXT_TO_ENCRYPT` environment variables +- `docker-compose up` + +## To run the application via docker +- Checkout the project +- `mvn package` +- `docker build -t 3des-algorithm_encryption . ` +- `docker run -it 3des-algorithm_encryption encryptionKey text` diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..20ad27c --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,5 @@ +3des-algorithm_encryption: + build: . + environment: + - ENCRYPTION_KEY="KDKDKDKD" + - TEXT_TO_ENCRYPT="some t3xt" \ No newline at end of file diff --git a/pom.xml b/pom.xml index 9f4bf02..a09b27b 100644 --- a/pom.xml +++ b/pom.xml @@ -10,15 +10,86 @@ 1.8 + 2.2 + 5.6.2 + 2.23.0 + 3.10 + UTF-8 + UTF-8 org.junit.jupiter junit-jupiter-api - 5.6.2 + ${junit-jupiter.version} test + + org.junit.jupiter + junit-jupiter-engine + ${junit-jupiter.version} + test + + + org.hamcrest + hamcrest + ${hamcrest.version} + provided + + + org.mockito + mockito-junit-jupiter + ${mockito-junit-jupiter.version} + test + + + org.apache.commons + commons-lang3 + ${commons-lang3.version} + + + + + org.apache.maven.plugins + maven-compiler-plugin + + ${java.version} + ${java.version} + + + + maven-assembly-plugin + + + package + + single + + + + + + + com.verygood.security.coding.CodingApplication + + + + jar-with-dependencies + + + + + org.apache.maven.plugins + maven-surefire-plugin + 2.22.0 + + -Dfile.encoding=UTF-8 + + + + + diff --git a/src/main/java/com/verygood/security/coding/CodingApplication.java b/src/main/java/com/verygood/security/coding/CodingApplication.java index 3854440..5d48a80 100644 --- a/src/main/java/com/verygood/security/coding/CodingApplication.java +++ b/src/main/java/com/verygood/security/coding/CodingApplication.java @@ -1,8 +1,49 @@ package com.verygood.security.coding; +import com.verygood.security.coding.api.Encryption; +import com.verygood.security.coding.api.IEncryptionKeyService; +import com.verygood.security.coding.api.exception.WrongInputArgumentsException; +import com.verygood.security.coding.service.EncryptionKeyServiceImpl; +import com.verygood.security.coding.service.TripleDesEncryptionImpl; +import org.apache.commons.lang3.StringUtils; + public class CodingApplication { - public static void main(String[] args) { - // TODO encrypt passed data with algorithm of your choice - } + static final String ENCRYPTION_KEY_ENV_VARIABLE = "ENCRYPTION_KEY"; + static final String TEXT_TO_ENCRYPT_ENV_VARIABLE = "TEXT_TO_ENCRYPT"; + private static String encryptionKey; + private static String textToEncrypt; + + public static void main(String[] args) { + setArguments(args); + + IEncryptionKeyService encryptionKeyService = new EncryptionKeyServiceImpl(encryptionKey); + Encryption encryptionService = new TripleDesEncryptionImpl(encryptionKeyService); + String encryptedText = encryptionService.encrypt(textToEncrypt); + String decryptedText = encryptionService.decrypt(encryptedText); + + System.out.println("Encrypted text: " + encryptedText); + System.out.println("Decrypted text: " + decryptedText); + } + + private static void setArguments(String[] args) { + if (args.length <= 1) { + readArgsFromEnvironmentVariables(); + if (StringUtils.isAllBlank(encryptionKey, textToEncrypt)) { + throw new WrongInputArgumentsException("Arguments are empty or half empty. Please input an encryption key and a text, " + + "e.g. ./application.jar FHDGYR 'text to encrypt'"); + } + } + else { + encryptionKey = args[0]; + textToEncrypt = args[1]; + } + } + + private static void readArgsFromEnvironmentVariables() { + String encryptionKeyFromEnv = System.getenv(ENCRYPTION_KEY_ENV_VARIABLE); + String textToEncryptFromEnv = System.getenv(TEXT_TO_ENCRYPT_ENV_VARIABLE); + encryptionKey = encryptionKeyFromEnv; + textToEncrypt = textToEncryptFromEnv; + } } diff --git a/src/main/java/com/verygood/security/coding/Encryption.java b/src/main/java/com/verygood/security/coding/api/Encryption.java similarity index 71% rename from src/main/java/com/verygood/security/coding/Encryption.java rename to src/main/java/com/verygood/security/coding/api/Encryption.java index 3f1a28c..3e04acb 100644 --- a/src/main/java/com/verygood/security/coding/Encryption.java +++ b/src/main/java/com/verygood/security/coding/api/Encryption.java @@ -1,4 +1,4 @@ -package com.verygood.security.coding; +package com.verygood.security.coding.api; public interface Encryption { String encrypt(String text); diff --git a/src/main/java/com/verygood/security/coding/api/IEncryptionKeyService.java b/src/main/java/com/verygood/security/coding/api/IEncryptionKeyService.java new file mode 100644 index 0000000..2eecff2 --- /dev/null +++ b/src/main/java/com/verygood/security/coding/api/IEncryptionKeyService.java @@ -0,0 +1,8 @@ +package com.verygood.security.coding.api; + +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; + +public interface IEncryptionKeyService { + byte[] getKey() throws NoSuchAlgorithmException, InvalidKeySpecException; +} diff --git a/src/main/java/com/verygood/security/coding/api/exception/WrongInputArgumentsException.java b/src/main/java/com/verygood/security/coding/api/exception/WrongInputArgumentsException.java new file mode 100644 index 0000000..3fb1220 --- /dev/null +++ b/src/main/java/com/verygood/security/coding/api/exception/WrongInputArgumentsException.java @@ -0,0 +1,8 @@ +package com.verygood.security.coding.api.exception; + +public class WrongInputArgumentsException extends RuntimeException { + + public WrongInputArgumentsException(String message) { + super(message); + } +} diff --git a/src/main/java/com/verygood/security/coding/service/EncryptionKeyServiceImpl.java b/src/main/java/com/verygood/security/coding/service/EncryptionKeyServiceImpl.java new file mode 100644 index 0000000..abde18d --- /dev/null +++ b/src/main/java/com/verygood/security/coding/service/EncryptionKeyServiceImpl.java @@ -0,0 +1,52 @@ +package com.verygood.security.coding.service; + +import com.verygood.security.coding.api.IEncryptionKeyService; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.PBEKeySpec; + +public class EncryptionKeyServiceImpl implements IEncryptionKeyService { + + private static final String PBKDF2_ALGORITHM = "PBKDF2WithHmacSHA1"; + private static final String SHA1PRNG_ALGORITHM = "SHA1PRNG"; + static final int KEY_SIZE = 24; + private byte[] encryptionKeyInBytes; + private String encryptionKey; + + public EncryptionKeyServiceImpl(String encryptionKey) { + this.encryptionKey = encryptionKey; + } + + @Override + public byte[] getKey() throws NoSuchAlgorithmException, InvalidKeySpecException { + if (encryptionKeyInBytes == null) { + encryptionKeyInBytes = generateKey(); + } + return encryptionKeyInBytes; + } + + private byte[] generateKey() throws NoSuchAlgorithmException, InvalidKeySpecException { + + if (encryptionKey == null) { + System.out.println("Encryption key is blank, using an autogenerated key"); + encryptionKey = ""; + } + + int iterations = 10; + char[] chars = encryptionKey.toCharArray(); + byte[] salt = getSalt(); + + PBEKeySpec spec = new PBEKeySpec(chars, salt, iterations, KEY_SIZE * 8); + SecretKeyFactory skf = SecretKeyFactory.getInstance(PBKDF2_ALGORITHM); + return skf.generateSecret(spec).getEncoded(); + } + + private static byte[] getSalt() throws NoSuchAlgorithmException { + SecureRandom sr = SecureRandom.getInstance(SHA1PRNG_ALGORITHM); + byte[] salt = new byte[16]; + sr.nextBytes(salt); + return salt; + } +} diff --git a/src/main/java/com/verygood/security/coding/service/TripleDesEncryptionImpl.java b/src/main/java/com/verygood/security/coding/service/TripleDesEncryptionImpl.java new file mode 100644 index 0000000..62ddebc --- /dev/null +++ b/src/main/java/com/verygood/security/coding/service/TripleDesEncryptionImpl.java @@ -0,0 +1,95 @@ +package com.verygood.security.coding.service; + +import com.verygood.security.coding.api.Encryption; +import com.verygood.security.coding.api.IEncryptionKeyService; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.InvalidKeySpecException; +import java.util.Base64; +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public class TripleDesEncryptionImpl implements Encryption { + + private static final Charset ENCODING = StandardCharsets.UTF_8; + private static final String ALGORITHM = "DESede"; + private static final String TRANSFORMATION = ALGORITHM + "/CBC/PKCS5Padding"; + private static final String SECURE_RANDOM_ALGORITHM = "SHA1PRNG"; + private final IEncryptionKeyService secretKeyService; + private IvParameterSpec ivParameterSpec; + private SecretKeySpec secretKeySpecs; + + public TripleDesEncryptionImpl(IEncryptionKeyService secretKeyService) { + this.secretKeyService = secretKeyService; + } + + @Override + public String encrypt(String text) { + if (text == null) { + return null; + } + byte[] bytes = new byte[0]; + try { + bytes = encryptDecryptInternal(text.getBytes(ENCODING), Cipher.ENCRYPT_MODE); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchPaddingException + | BadPaddingException | IllegalBlockSizeException | InvalidKeySpecException e) { + e.printStackTrace(); + } + return Base64.getEncoder().encodeToString(bytes); + } + + @Override + public String decrypt(String text) { + if (text == null) { + return null; + } + byte[] textDecoded = Base64.getDecoder().decode(text); + byte[] bytes = new byte[0]; + try { + bytes = encryptDecryptInternal(textDecoded, Cipher.DECRYPT_MODE); + } catch (NoSuchAlgorithmException | InvalidAlgorithmParameterException | InvalidKeyException | NoSuchPaddingException + | BadPaddingException | IllegalBlockSizeException | InvalidKeySpecException e) { + e.printStackTrace(); + } + return new String(bytes, ENCODING); + } + + private byte[] encryptDecryptInternal(byte[] text, int mode) throws NoSuchAlgorithmException, + InvalidAlgorithmParameterException, InvalidKeyException, + NoSuchPaddingException, BadPaddingException, IllegalBlockSizeException, InvalidKeySpecException { + + SecretKey key = getSecretKey(); + IvParameterSpec iv = getIvParameterSpec(); + Cipher cipher = Cipher.getInstance(TRANSFORMATION); + cipher.init(mode, key, iv); + + return cipher.doFinal(text); + } + + private SecretKeySpec getSecretKey() throws NoSuchAlgorithmException, InvalidKeySpecException { + if (secretKeySpecs == null) { + secretKeySpecs = new SecretKeySpec(secretKeyService.getKey(), ALGORITHM); + } + return secretKeySpecs; + } + + private IvParameterSpec getIvParameterSpec() throws NoSuchAlgorithmException { + if (ivParameterSpec == null) { + SecureRandom randomSecureRandom = SecureRandom.getInstance(SECURE_RANDOM_ALGORITHM); + byte[] bytes = new byte[8]; + randomSecureRandom.nextBytes(bytes); + ivParameterSpec = new IvParameterSpec(bytes); + } + return ivParameterSpec; + } + +} diff --git a/src/test/java/com/verygood/security/coding/CodingApplicationTests.java b/src/test/java/com/verygood/security/coding/CodingApplicationTests.java index bec028b..9a37691 100644 --- a/src/test/java/com/verygood/security/coding/CodingApplicationTests.java +++ b/src/test/java/com/verygood/security/coding/CodingApplicationTests.java @@ -1,12 +1,70 @@ package com.verygood.security.coding; +import static org.junit.jupiter.api.Assertions.assertThrows; + +import com.verygood.security.coding.api.exception.WrongInputArgumentsException; import org.junit.jupiter.api.Test; class CodingApplicationTests { - // TODO add test cases + @Test + void applicationStartsWithoutErrors() { + String[] args = new String[]{"encryptionKey", "textToEncrypt"}; + CodingApplication.main(args); + } + + @Test + void applicationEncryptsDecryptsRussian() { + String[] args = new String[]{"encryptionKey", "русский"}; + CodingApplication.main(args); + } + + @Test + void applicationEncryptsDecryptsRussian_whenKeyInRussian() { + String[] args = new String[]{"русскийКлюч", "русский"}; + CodingApplication.main(args); + } + + @Test + void applicationStartsWithoutErrors_ifKeyAndTextAreEmpty() { + String[] args = new String[]{"", ""}; + CodingApplication.main(args); + } + + @Test + void applicationStartsWithoutErrors_ifKeyAndTextAreBlank() { + String[] args = new String[]{" ", " "}; + CodingApplication.main(args); + } + + @Test + void applicationStartsWithoutErrors_ifKeyIsBlank_whileTextHasSomeValue() { + String[] args = new String[]{" ", "test"}; + CodingApplication.main(args); + } + + @Test + void applicationStartsWithoutErrors_ifKeyNotBlank_whileTextIsBlank() { + String[] args = new String[]{"test", " "}; + CodingApplication.main(args); + } + + + @Test + void applicationThrowsWrongInputArgumentsException_ifOnlyOneArgPassed() { + assertThrows(WrongInputArgumentsException.class, + ()->{ + String[] args = new String[]{"encryptionKey"}; + CodingApplication.main(args); + }); + } @Test - void testSomething() { + void applicationThrowsWrongInputArgumentsException_ifNoArgPassed() { + assertThrows(WrongInputArgumentsException.class, + ()->{ + String[] args = new String[]{}; + CodingApplication.main(args); + }); } } diff --git a/src/test/java/com/verygood/security/coding/service/EncryptionKeyServiceImplTest.java b/src/test/java/com/verygood/security/coding/service/EncryptionKeyServiceImplTest.java new file mode 100644 index 0000000..2003d31 --- /dev/null +++ b/src/test/java/com/verygood/security/coding/service/EncryptionKeyServiceImplTest.java @@ -0,0 +1,61 @@ +package com.verygood.security.coding.service; + +import static com.verygood.security.coding.service.EncryptionKeyServiceImpl.KEY_SIZE; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; + +import java.nio.charset.StandardCharsets; +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import org.junit.jupiter.api.Test; + +class EncryptionKeyServiceImplTest { + + private EncryptionKeyServiceImpl encryptionKeyService; + + @Test + void getKey_returns24BytesKey() throws InvalidKeySpecException, NoSuchAlgorithmException { + // given + String keyInString = "test"; + encryptionKeyService = new EncryptionKeyServiceImpl(keyInString); + + // when + byte[] actualEncryptionKey = encryptionKeyService.getKey(); + + // then + assertThat(actualEncryptionKey.length, is(KEY_SIZE)); + String keyAfterGeneration = new String(actualEncryptionKey, StandardCharsets.UTF_8); + assertThat(keyInString, is(not(keyAfterGeneration))); + } + + @Test + void getKey_returns24BytesKeyForNull() throws InvalidKeySpecException, NoSuchAlgorithmException { + // given + String keyInString = null; + encryptionKeyService = new EncryptionKeyServiceImpl(keyInString); + + // when + byte[] actualEncryptionKey = encryptionKeyService.getKey(); + + // then + assertThat(actualEncryptionKey.length, is(KEY_SIZE)); + String keyAfterGeneration = new String(actualEncryptionKey, StandardCharsets.UTF_8); + assertThat(keyInString, is(not(keyAfterGeneration))); + } + + @Test + void getKey_returns24BytesKeyForEmptyString() throws InvalidKeySpecException, NoSuchAlgorithmException { + // given + String keyInString = ""; + encryptionKeyService = new EncryptionKeyServiceImpl(keyInString); + + // when + byte[] actualEncryptionKey = encryptionKeyService.getKey(); + + // then + assertThat(actualEncryptionKey.length, is(KEY_SIZE)); + String keyAfterGeneration = new String(actualEncryptionKey, StandardCharsets.UTF_8); + assertThat(keyInString, is(not(keyAfterGeneration))); + } +} \ No newline at end of file diff --git a/src/test/java/com/verygood/security/coding/service/TripleDesEncryptionImplTest.java b/src/test/java/com/verygood/security/coding/service/TripleDesEncryptionImplTest.java new file mode 100644 index 0000000..829da2e --- /dev/null +++ b/src/test/java/com/verygood/security/coding/service/TripleDesEncryptionImplTest.java @@ -0,0 +1,116 @@ +package com.verygood.security.coding.service; + +import static com.verygood.security.coding.service.EncryptionKeyServiceImpl.KEY_SIZE; +import static org.hamcrest.CoreMatchers.is; +import static org.hamcrest.CoreMatchers.not; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.text.IsEmptyString.emptyString; +import static org.mockito.BDDMockito.given; + +import java.security.NoSuchAlgorithmException; +import java.security.spec.InvalidKeySpecException; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class TripleDesEncryptionImplTest { + + private static final String STRING_TO_ENCRYPT = "test"; + @Mock + EncryptionKeyServiceImpl secretKeyService; + @InjectMocks + private TripleDesEncryptionImpl tripleDesEncryption; + + @Test + void encrypt_encryptsData() throws InvalidKeySpecException, NoSuchAlgorithmException { + // given + given(secretKeyService.getKey()).willReturn(new byte[KEY_SIZE]); + + // when + String encryptedText = tripleDesEncryption.encrypt(STRING_TO_ENCRYPT); + + // then + assertThat(encryptedText, is(not(emptyString()))); + assertThat(encryptedText, is(not(STRING_TO_ENCRYPT))); + + } + + @Test + void encrypt_returnsTheSameValueOnMultipleInvocations() throws InvalidKeySpecException, NoSuchAlgorithmException { + // given + given(secretKeyService.getKey()).willReturn(new byte[KEY_SIZE]); + + // when + String firstEncryption = tripleDesEncryption.encrypt(STRING_TO_ENCRYPT); + String secondEncryption = tripleDesEncryption.encrypt(STRING_TO_ENCRYPT); + + // then + assertThat(firstEncryption, is(not(emptyString()))); + assertThat(secondEncryption, is(not(emptyString()))); + assertThat(firstEncryption, is((secondEncryption))); + + } + + @Test + void decrypt_decryptsData() throws InvalidKeySpecException, NoSuchAlgorithmException { + // given + given(secretKeyService.getKey()).willReturn(new byte[KEY_SIZE]); + + // when + String encryptedText = tripleDesEncryption.encrypt(STRING_TO_ENCRYPT); + String decryptedText = tripleDesEncryption.decrypt(encryptedText); + + // then + assertThat(decryptedText, is(not(emptyString()))); + assertThat(decryptedText, is(STRING_TO_ENCRYPT)); + + } + + @Test + void decrypt_ProperlyDecryptsRussian() throws InvalidKeySpecException, NoSuchAlgorithmException { + // given + given(secretKeyService.getKey()).willReturn(new byte[KEY_SIZE]); + String expectedText = "русский"; + + // when + String encryptedText = tripleDesEncryption.encrypt(expectedText); + String decryptedText = tripleDesEncryption.decrypt(encryptedText); + + // then + assertThat(decryptedText, is(not(emptyString()))); + assertThat(decryptedText, is(expectedText)); + + } + + @Test + void encrypt_properlyEncryptsEmptyString() throws InvalidKeySpecException, NoSuchAlgorithmException { + // given + given(secretKeyService.getKey()).willReturn(new byte[KEY_SIZE]); + String expectedString = ""; + + // when + String encryptedText = tripleDesEncryption.encrypt(expectedString); + String decryptedText = tripleDesEncryption.decrypt(encryptedText); + + // then + assertThat(decryptedText, is(expectedString)); + + } + + @Test + void encrypt_returnsNullOnNullString() { + // given + String expectedString = null; + + // when + String encryptedText = tripleDesEncryption.encrypt(expectedString); + String decryptedText = tripleDesEncryption.decrypt(encryptedText); + + // then + assertThat(decryptedText, is(expectedString)); + + } +} \ No newline at end of file