diff --git a/README.md b/README.md index ef5e93f..711e15d 100644 --- a/README.md +++ b/README.md @@ -1,55 +1,48 @@ # flexcaptcha -A minimalistic CAPTCHA generator and validator, with customizable rendering options ready for both web and desktop applications. The image manipulation is done through [https://github.com/ajmas/JH-Labs-Java-Image-Filters](https://github.com/ajmas/JH-Labs-Java-Image-Filters). + +A minimalistic CAPTCHA generator and validator, with customizable rendering options ready for both +web and desktop +applications. The image manipulation is done +through [https://github.com/ajmas/JH-Labs-Java-Image-Filters](https://github.com/ajmas/JH-Labs-Java-Image-Filters). ## Usage -### text-based CAPTCHA: ```java - SimpleCaptchaTextGenerator generator = new SimpleCaptchaTextGenerator(); //Can generate randomized strings from a pool of allowed characters - String s = generator.generate(10, Case.UPPERCASE); //Here is my random string. I want all letters to be uppercase. lowercase and mixed-case is supported, too. Or you supply your own string. - String pw = "ThisIsMyPassword"; //Supply a password for encryption - - SimpleTextImageRenderer renderer = new SimpleTextImageRenderer(); //pick a renderer controlling the image generation (and distortion) - CipherHandler ch = new CipherHandler(); //Cipherhandler for implementing the encryption and decryption - - TextCaptchaHandler handler = new SimpleTextCaptchaHandler(); - String saltSource = "Hello World!"; //A salt source for salting the hashes and encryption - TextCaptcha captcha = handler.toCaptcha(s, ch, saltSource, pw, renderer , 100, 300); //putting it all together +AbstractCaptchaRenderer renderer=CaptchaRenderer.getDefaultCaptchaRenderer(); + AbstractCaptchaCipher captchaCipher=CaptchaCipher.builder() + .expirationTimeSettings(new ExpirationTimeSettings(560L,()->System.currentTimeMillis())) + .build(); + + AbstractCaptchaGenerator generator=new CaptchaGenerator(captchaCipher,renderer); + String solution="abc123"; + String salt="someSerializable"; + Captcha captcha=generator.generate(solution,salt); ``` -#### Sample images: - -![5W3QRKCYMY](https://user-images.githubusercontent.com/96397624/148242974-931e21b9-de0c-4200-ad99-41c3e3918228.png) - -![B6JJRT9XSD](https://user-images.githubusercontent.com/96397624/148242976-62a6e567-f2e0-43cf-87ac-43ea03aef6a9.png) - -![bbmsjgwf4w](https://user-images.githubusercontent.com/96397624/148242978-1037e9a1-7b19-48e7-86e3-8896bb33306d.png) -![FqF](https://user-images.githubusercontent.com/96397624/148242981-d7889d63-5850-40a7-b913-9f66b9fe478d.png) - -![m43geumhk8](https://user-images.githubusercontent.com/96397624/148242983-53876334-f87f-483e-93c9-9f63ff958e8e.png) +Alternatively, an arbitrary chain of effects can be used to further manipulate the rendered image: +```java +TwirlFilter filter=new TwirlFilter(); + float twirlStrength=-0.3f; + filter.setAngle(twirlStrength); + AbstractCaptchaRenderer renderer=CaptchaRenderer.builder() + .imageOperationsList(Collections.singletonList(filter)) + .build(); -### image-based CAPTCHA: + AbstractCaptchaCipher captchaCipher=CaptchaCipher.builder() + .expirationTimeSettings(new ExpirationTimeSettings(560L,()->System.currentTimeMillis())) + .build(); -```java - ImageCaptchaHandler handler = new SimpleImageCaptchaHandler(); - CipherHandler ch = new CipherHandler(); - ImageLoader loader = new ImageLoader(); - - BufferedImage[] solutionImages = loader.getImagesfromPath("C:\\SomeDirectory"); - BufferedImage[] fillImages = loader.getImagesfromPath("C:\\SomeOtherDirectory"); - - String saltSource = "Hello World!"; - int gridWidth = 3; - ImageCaptcha captcha = handler.generate(gridWidth, ch, saltSource, password, solutionImages, fillImages); + AbstractCaptchaGenerator generator=new CaptchaGenerator(captchaCipher,renderer); + String solution=generateCaptchaSolution(); + Captcha captcha=generator.generate(solution,salt); ``` -## Dependency ``` io.github.yaforster flexcaptcha - 1.2.8 + 2.0.0 ``` diff --git a/docs/index.md b/docs/index.md index 72d9629..7cd2cc7 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1,11 +1,23 @@ -A minimalistic and performant CAPTCHA generator and validator, with customizable rendering options ready for both web and desktop applications. -The image manipulation is done through [https://github.com/ajmas/JH-Labs-Java-Image-Filters](https://github.com/ajmas/JH-Labs-Java-Image-Filters). +A minimalistic and performant CAPTCHA generator and validator, with customizable rendering options +ready for both web +and desktop applications. +The image manipulation is done +through [https://github.com/ajmas/JH-Labs-Java-Image-Filters](https://github.com/ajmas/JH-Labs-Java-Image-Filters). -CAPTCHAs generated by flexcaptcha are highly customizable and can be generated and validated in different ways through the same application simultaneously. Thanks to this, flexcaptcha can be used as a centralized service for your web application landscape, or as a microservice, to hand out and validate CAPTCHAs that are tailored to each individual application that uses it. +CAPTCHAs generated by flexcaptcha are highly customizable and can be generated and validated in +different ways through +the same application simultaneously. Thanks to this, flexcaptcha can be used as a centralized +service for your web +application landscape, or as a microservice, to hand out and validate CAPTCHAs that are tailored to +each individual +application that uses it. -The generated image data and tokens are not permanently stored in a database, improving response times and ease of use in your application besides not requiring additional infrastructure to set up. +The generated image data and tokens are not permanently stored in a database, improving response +times and ease of use +in your application besides not requiring additional infrastructure to set up. # Usage + ## text-based CAPTCHA: ```java @@ -20,6 +32,7 @@ The generated image data and tokens are not permanently stored in a database, im String saltSource = "Hello World!"; //A salt source for salting the hashes and encryption TextCaptcha captcha = handler.toCaptcha(s, ch, saltSource, pw, renderer , 100, 300); //putting it all together ``` + ### Sample images: ![5W3QRKCYMY](https://user-images.githubusercontent.com/96397624/148242974-931e21b9-de0c-4200-ad99-41c3e3918228.png) @@ -46,6 +59,7 @@ The generated image data and tokens are not permanently stored in a database, im int gridWidth = 3; ImageCaptcha captcha = handler.generate(gridWidth, ch, saltSource, password, solutionImages, fillImages); ``` + # Dependency ``` diff --git a/pom.xml b/pom.xml index 064ac62..e0de813 100644 --- a/pom.xml +++ b/pom.xml @@ -1,146 +1,141 @@ - - 4.0.0 flexcaptcha - io.github.yaforster - Flexible Captcha - Simple Captcha generation and validation - 1.2.8 - https://github.com/yaforster/flexcaptcha - - - Apache License, Version 2.0 - https://www.apache.org/licenses/LICENSE-2.0.txt - repo - - - - scm:git@github.com:4ster1751/flexcaptcha.git - https://github.com/4ster1751/flexcaptcha.git - - - - yaforster - Yannick Forster - forstery1751@gmail.com - - developer - - Europe/Berlin - - - - UTF-8 - ${basedir}/../.. - 11 - 11 - --illegal-access=permit - - - - - org.apache.logging.log4j - log4j-core - 2.20.0 - - - - junit - junit - 4.13.2 - test - - - - org.projectlombok - lombok - 1.18.28 - provided - - - - org.apache.commons - commons-lang3 - 3.12.0 - - - - com.jhlabs - filters - 2.0.235-1 - - - - org.mockito - mockito-core - 5.4.0 - test - - - - - ossrh - https://s01.oss.sonatype.org/content/repositories/snapshots - - - org.sonatype.plugins nexus-staging-maven-plugin - 1.6.13 - true - ossrh - https://s01.oss.sonatype.org/ true + https://s01.oss.sonatype.org/ + ossrh + true + org.sonatype.plugins + 1.6.13 - org.apache.maven.plugins maven-source-plugin - 3.3.0 - attach-sources jar-no-fork + attach-sources + org.apache.maven.plugins + 3.3.0 - org.apache.maven.plugins maven-javadoc-plugin - 3.5.0 - attach-javadocs jar + attach-javadocs + org.apache.maven.plugins + 3.6.2 - org.apache.maven.plugins maven-gpg-plugin - 3.1.0 - sign-artifacts - verify sign + sign-artifacts + verify + org.apache.maven.plugins + 3.1.0 + + + + lombok + org.projectlombok + provided + 1.18.30 + + + + commons-lang3 + org.apache.commons + 3.14.0 + + + + filters + com.jhlabs + 2.0.235-1 + + + + mockito-core + org.mockito + test + 5.8.0 + + + + org.junit.jupiter + junit-jupiter + 5.10.1 + test + + + Simple Captcha generation and validation + + + forstery1751@gmail.com + yaforster + Yannick Forster + + developer + + Europe/Berlin + + + + + ossrh + https://s01.oss.sonatype.org/content/repositories/snapshots + + + io.github.yaforster + + + repo + Apache License, Version 2.0 + https://www.apache.org/licenses/LICENSE-2.0.txt + + + 4.0.0 + Flexible Captcha + + --illegal-access=permit + ${basedir}/../.. + 21 + 21 + UTF-8 + + + scm:git@github.com:4ster1751/flexcaptcha.git + https://github.com/4ster1751/flexcaptcha.git + + https://github.com/yaforster/flexcaptcha + 2.0.0 diff --git a/src/main/java/io/github/yaforster/flexcaptcha/Captcha.java b/src/main/java/io/github/yaforster/flexcaptcha/Captcha.java deleted file mode 100644 index 749eaf5..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/Captcha.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.github.yaforster.flexcaptcha; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.Setter; - -/** - * Object representing a captcha output consisting of the image data to display - * and transport the actual captcha, and a token representing the hashed and - * salted solution - * - * @author Yannick Forster - */ - -@Getter -@Setter -@AllArgsConstructor -public abstract class Captcha { - /** - * String containing the hash of the original solution salted with a specified - * object - */ - private String token; -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/CaptchaHandler.java b/src/main/java/io/github/yaforster/flexcaptcha/CaptchaHandler.java deleted file mode 100644 index ff976ef..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/CaptchaHandler.java +++ /dev/null @@ -1,126 +0,0 @@ -package io.github.yaforster.flexcaptcha; - -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.util.Base64; - -/** - * @author Yannick Forster - * This interface defines the basic functionality shared amongst all - * Captcha handlers consisting of the adding of the handler reference to - * the token itself as well as the validation method definition and - * token creation implementation, the validation and - * convertion of a salt object to byte arrays - */ -public interface CaptchaHandler { - - /** - * Log4J Logger - */ - Logger log = LogManager.getLogger(CaptchaHandler.class); - - /** - * The algorithm used to hash - */ - String ALGORITHM_NAME = "SHA-256"; - /** - * The delimiter used to differentiate between the hashed portion of the token - * and the self reference of the handler - */ - String DELIMITER = "###"; - - /** - * Appends a given token with an encrypted self reference used for validation at - * a later point. This is required because each handler implementation allows - * for a customized validation and token generation logic, and validation of a - * token can not be done reliably without knowing the implementation that - * created it. This method encrypts the fully qualified name of the - * implementation and appends it to the token. The {@link Validator} class can - * be used to decrypt the token, instantiate the CaptchaHandler implementation - * and run its validation. - * - * @param cipherHandler {@link CipherHandler} object used to handle the - * encrypting of the self reference - * @param saltSource the salt source used for the encryption. - * @param password the password used to encrypt the implementation - * reference - * @return appended token string - */ - default String addSelfReference(CipherHandler cipherHandler, Serializable saltSource, - String password) { - byte[] ivBytes = cipherHandler.generateIV().getIV(); - byte[] encryptedBytes = cipherHandler.encryptString(this.getClass().getName().getBytes(), password, saltSource, - ivBytes); - String base64 = Base64.getEncoder().encodeToString(encryptedBytes); - return DELIMITER + base64; - } - - /** - * Validates the answer to the captcha based on the token and the salt object. - * Returns true if the answer is correct and the token authentic - * - * @param answer the given solution to the captcha to be validated - * @param cipherHandler {@link CipherHandler} object used to handle the - * decryption of the self reference - * @param token the returned token originally created with the captcha - * @param saltSource the salt source originally used to salt the hashed - * solution to create the token. Will be used again to - * validate the answer - * @param password The password string used for decryption - * @return boolean whether the captcha is valid - */ - boolean validate(String answer, String token, CipherHandler cipherHandler, Serializable saltSource, - String password); - - /** - * Creates the token based on the captcha solution and the object to be used for - * salting - * - * @param sourceString captcha solution as string - * @param saltSource arbitrary object to be used to salt the solution hash for - * added security and to allow for authenticating the given - * answer - * @return String of the token - */ - default String makeToken(String sourceString, Serializable saltSource) { - byte[] captchaTextBytes = sourceString.getBytes(); - byte[] saltObjectBytes = getSaltObjectBytes(saltSource); - try { - MessageDigest md = MessageDigest.getInstance(ALGORITHM_NAME); - md.update(captchaTextBytes); - md.update(saltObjectBytes); - byte[] outputBytes = md.digest(); - return Base64.getEncoder().encodeToString(outputBytes); - } catch (NoSuchAlgorithmException e) { - log.error("Error creating the captcha token: " + e.getMessage()); - return null; - } - } - - /** - * Converts the given object to a byte array - * - * @param saltSource object to be used as salt - * @return byte array of the object - */ - default byte[] getSaltObjectBytes(Serializable saltSource) { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(baos)) { - oos.writeObject(saltSource); - baos.close(); - oos.close(); - return baos.toByteArray(); - } catch (IOException e) { - log.error("Error converting the salt object source to byte array: " + e.getMessage()); - return null; - } - } - -} \ No newline at end of file diff --git a/src/main/java/io/github/yaforster/flexcaptcha/CipherHandler.java b/src/main/java/io/github/yaforster/flexcaptcha/CipherHandler.java deleted file mode 100644 index 7c0683a..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/CipherHandler.java +++ /dev/null @@ -1,181 +0,0 @@ -package io.github.yaforster.flexcaptcha; - -import org.apache.commons.lang3.StringUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import javax.crypto.*; -import javax.crypto.spec.IvParameterSpec; -import javax.crypto.spec.PBEKeySpec; -import javax.crypto.spec.SecretKeySpec; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectOutputStream; -import java.io.Serializable; -import java.security.InvalidAlgorithmParameterException; -import java.security.InvalidKeyException; -import java.security.NoSuchAlgorithmException; -import java.security.SecureRandom; -import java.security.spec.InvalidKeySpecException; -import java.security.spec.KeySpec; -import java.util.Arrays; - -/** - * Handles String encryption or decryption. - * - * @author Yannick Forster - */ -public class CipherHandler { - - /** - * The encryption algorithm - */ - static final String ALGORITHM = "PBKDF2WithHmacSHA256"; - /** - * The cipher algorithm - */ - static final String CIPERALGORITHM = "AES/CBC/PKCS5Padding"; - /** - * AES - */ - static final String AES = "AES"; - /** - * Log4J Logger - */ - private final Logger log = LogManager.getLogger(CipherHandler.class); - - /** - * Gets the byte array of the salt source object - * - * @param saltSource object to be used as salt - * @return byte array of the given object - */ - private static byte[] getSaltBytes(Serializable saltSource) throws IOException { - byte[] saltBytes; - try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream out = new ObjectOutputStream(bos)) { - out.writeObject(saltSource); - saltBytes = bos.toByteArray(); - return saltBytes; - } - } - - /** - * generates a new initialization vector as randomized 16 bytes and returns it - * as {@link IvParameterSpec} - * - * @return randomized {@link IvParameterSpec} - */ - public IvParameterSpec generateIV() { - byte[] iv = new byte[16]; - new SecureRandom().nextBytes(iv); - return new IvParameterSpec(iv); - } - - /** - * Generates and configures the {@link Cipher} object used for encryption and - * decryption. - * - * @param password the password used for encryption - * @param saltSource a Serializable object used as salt - * @param mode specifies whether the cipher will encrypt or decrypt - * @param ivBytes the initialization vector - * @return configured Cipher object - */ - private Cipher getCipher(String password, Serializable saltSource, int mode, byte[] ivBytes) - throws NoSuchAlgorithmException, NoSuchPaddingException, IOException, InvalidKeySpecException, - InvalidKeyException, InvalidAlgorithmParameterException { - IvParameterSpec iv = new IvParameterSpec(ivBytes); - SecretKeyFactory factory = SecretKeyFactory.getInstance(ALGORITHM); - Cipher cipher = Cipher.getInstance(CIPERALGORITHM); - byte[] saltBytes = getSaltBytes(saltSource); - KeySpec ks = new PBEKeySpec(password.toCharArray(), saltBytes, 65536, 256); - SecretKey key = new SecretKeySpec(factory.generateSecret(ks).getEncoded(), AES); - cipher.init(mode, key, iv); - return cipher; - } - - /** - * Encrypts a given String with a password and a salt source. To encrypt it, an - * initialization vector is generated and used to encrypt the string. The - * initialization vector is then put in front of the encrypted byte array for - * transportation, so it can be used to decrypt the byte array after it (using - * the same password and salt) at a later point. - * - * @param input the input byte array to be encrypted - * @param password the password used for encryption - * @param saltSource a Serializable object used as salt - * @return the encrypted string - */ - public final byte[] encryptString(byte[] input, String password, Serializable saltSource) { - return encryptString(input, password, saltSource, generateIV().getIV()); - } - - /** - * Encrypts a given String with a password and a salt source. To encrypt it, an - * initialization vector is generated and used to encrypt the string. The - * initialization vector is then put in front of the encrypted byte array for - * transportation, so it can be used to decrypt the byte array after it (using - * the same password and salt) at a later point. - * - * @param input the input byte array to be encrypted - * @param password the password used for encryption - * @param saltSource a Serializable object used as salt - * @param ivBytes 16 randomly generated bytes used for the encryption - * @return the encrypted string - */ - public final byte[] encryptString(byte[] input, String password, Serializable saltSource, byte[] ivBytes) { - if (StringUtils.isEmpty(password)) { - throw new IllegalArgumentException("The supplied password is empty or null. Encryption can not proceed."); - } - try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { - Cipher cipher = getCipher(password, saltSource, Cipher.ENCRYPT_MODE, ivBytes); - byte[] cipherBytes = cipher.doFinal(input); - outputStream.write(ivBytes); - outputStream.write(cipherBytes); - return outputStream.toByteArray(); - } catch (IOException e) { - log.fatal("Fatal error producing byte array data of the salt source object: " + e.getLocalizedMessage()); - } catch (NoSuchAlgorithmException e) { - log.fatal("Unknown algorithm specified for token encryption: " + e.getLocalizedMessage()); - } catch (NoSuchPaddingException e) { - log.fatal("Unknown padding specified for token encryption: " + e.getLocalizedMessage()); - } catch (InvalidKeySpecException | InvalidKeyException | InvalidAlgorithmParameterException - | IllegalBlockSizeException | BadPaddingException e) { - log.fatal("Fatal error during token encryption: " + e.getLocalizedMessage()); - } - return null; - } - - /** - * Decrypts an encrypted string using a specified password and salt source - * object. The decrypted string must contain the initialization vector as the - * first 16 bytes as they will be extracted to be used in the decryption. - * - * @param input the input byte array to be decrypted - * @param password the password used for encryption - * @param saltSource a Serializable object used as salt - * @return the decrypted string - */ - public final byte[] decryptString(byte[] input, String password, Serializable saltSource) { - if (StringUtils.isEmpty(password)) { - throw new IllegalArgumentException("The supplied password is empty or null. Encryption can not proceed."); - } - try { - byte[] ivBytes = Arrays.copyOfRange(input, 0, 16); - Cipher cipher = getCipher(password, saltSource, Cipher.DECRYPT_MODE, ivBytes); - byte[] cipherBytes = Arrays.copyOfRange(input, 16, input.length); - return cipher.doFinal(cipherBytes); - } catch (IOException e) { - log.fatal("Fatal error producing byte array data of the salt source object: " + e.getLocalizedMessage()); - } catch (NoSuchAlgorithmException e) { - log.fatal("Unknown algorithm specified for token decryption: " + e.getLocalizedMessage()); - } catch (NoSuchPaddingException e) { - log.fatal("Unknown padding specified for token decryption: " + e.getLocalizedMessage()); - } catch (InvalidKeySpecException | InvalidKeyException | InvalidAlgorithmParameterException - | IllegalBlockSizeException | BadPaddingException e) { - log.fatal("Fatal error during token decryption: " + e.getLocalizedMessage()); - } - return null; - } - -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/Validator.java b/src/main/java/io/github/yaforster/flexcaptcha/Validator.java deleted file mode 100644 index de5d27f..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/Validator.java +++ /dev/null @@ -1,65 +0,0 @@ -package io.github.yaforster.flexcaptcha; - -import lombok.NonNull; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import java.io.Serializable; -import java.lang.reflect.Constructor; -import java.lang.reflect.InvocationTargetException; -import java.util.Base64; - -/** - * Handles the general validation of potentially unknown tokens. Will split the - * token at the delimiter and decrypt the self reference added by a handler for - * further individual validation of the token generated by this handler - * - * @author Yannick Forster - */ -public class Validator { - public static final Object[] INITARGS = {}; - /** - * Log4J Logger - */ - private final Logger log = LogManager.getLogger(Validator.class); - - /** - * Validates the given answer by comparing it to the token if hashed and salted - * the same way. The passed ciperHandler will decrypt the CaptchaHandler - * implementation and run its validation method. - * - * @param input the given answer to be validated - * @param token the returned token generated with the original captcha - * @param saltSource the salt source used to generate the captcha - * @param password the password used to encrypt the implementation - * reference - * @param cipherHandler Cipherhandler object used to decrypt the name of the - * implementation from the token - * @return {@link Boolean} of the validation result. null if the validation - * encountered a problem. - */ - public Boolean validateInput(@NonNull final String input, @NonNull final CipherHandler cipherHandler, @NonNull final String token, @NonNull final Serializable saltSource, - @NonNull final String password) { - try { - String splitString = token.split(CaptchaHandler.DELIMITER)[1]; - byte[] splitStringBytes = Base64.getDecoder().decode(splitString); - byte[] decryptedBytes = cipherHandler.decryptString(splitStringBytes, password, saltSource); - if (decryptedBytes != null) { - String decryptedName = new String(decryptedBytes); - Class handler = Class.forName(decryptedName); - Constructor constructor = handler.getConstructor(); - CaptchaHandler instanceOfMyClass = (CaptchaHandler) constructor.newInstance(INITARGS); - return instanceOfMyClass.validate(input, token.split(CaptchaHandler.DELIMITER)[0], cipherHandler, - saltSource, password); - } - } catch (ClassNotFoundException | NoSuchMethodException | SecurityException | InstantiationException - | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) { - log.fatal( - "Could not instantiate CaptchaHandler implementation from decrypted token. Validation can not proceed."); - e.printStackTrace(); - - } - return null; - } - -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaCipher.java b/src/main/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaCipher.java new file mode 100644 index 0000000..c5d45a1 --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaCipher.java @@ -0,0 +1,117 @@ +package io.github.yaforster.flexcaptcha.core; + +import io.github.yaforster.flexcaptcha.impl.token.CipherInstantiationException; +import io.github.yaforster.flexcaptcha.impl.token.CipherSettings; +import io.github.yaforster.flexcaptcha.impl.token.ExpirationTimeSettings; +import io.github.yaforster.flexcaptcha.impl.token.TokengenerationException; +import lombok.AllArgsConstructor; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.ObjectOutputStream; +import java.io.Serializable; +import java.security.GeneralSecurityException; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.security.spec.KeySpec; + +@AllArgsConstructor +public abstract class AbstractCaptchaCipher { + + protected CipherSettings cipherSettings; + protected String encryptionPassword; + protected ExpirationTimeSettings expirationTimeSettings; + + /** + * generates a new initialization vector as randomized 16 bytes and returns it as + * {@link IvParameterSpec} + * + * @return randomized {@link IvParameterSpec} + */ + public static IvParameterSpec generateIV() { + byte[] iv = new byte[16]; + new SecureRandom().nextBytes(iv); + return new IvParameterSpec(iv); + } + + /** + * Generates and configures the {@link Cipher} object used for encryption and decryption. + * + * @param password the password used for encryption + * @param saltSource a Serializable object used as salt + * @param mode specifies whether the cipher will encrypt or decrypt + * @param ivBytes the initialization vector + * @return configured Cipher object + */ + protected final Cipher getCipher(final String password, final Serializable saltSource, final int mode, + final byte[] ivBytes) { + try { + IvParameterSpec iv = new IvParameterSpec(ivBytes); + SecretKeyFactory factory = SecretKeyFactory.getInstance(cipherSettings.encryptionAlgorithm()); + Cipher cipher = Cipher.getInstance(cipherSettings.cipherAlgorithm()); + byte[] saltBytes = getSaltBytes(saltSource); + KeySpec ks = new PBEKeySpec(password.toCharArray(), saltBytes, 65536, 256); + SecretKey key = new SecretKeySpec(factory.generateSecret(ks) + .getEncoded(), cipherSettings.secretKeySpecAlgorithm()); + cipher.init(mode, key, iv); + return cipher; + } + catch (GeneralSecurityException originalException) { + throw mapCipherGenerationException(originalException); + } + } + + /** + * Gets the byte array of the salt source object + * + * @param saltSource object to be used as salt + * @return byte array of the given object + */ + public static byte[] getSaltBytes(final Serializable saltSource) { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream(); ObjectOutputStream out = + new ObjectOutputStream(bos)) { + out.writeObject(saltSource); + return bos.toByteArray(); + } + catch (IOException e) { + throw new TokengenerationException("Fatal error during convertion of salt into byte array."); + } + } + + private CipherInstantiationException mapCipherGenerationException(GeneralSecurityException originalException) { + String errorMessage = switch (originalException) { + case NoSuchPaddingException nspe -> + "Unknown padding specified for token encryption: " + nspe.getLocalizedMessage(); + case NoSuchAlgorithmException nsae -> + "Unknown padding specified for token encryption: " + nsae.getLocalizedMessage(); + default -> "Fatal error during token encryption: " + originalException.getLocalizedMessage(); + }; + return new CipherInstantiationException(errorMessage, originalException); + } + + /** + * Generates a token out of the given captcha solution and another object acting as the salt. + * + * @param captchaSolution the actual captcha solution. + * @param saltSource a {@link Serializable} + * @return the generated token to be returned later for validation alongside the user input + */ + public abstract String generateToken(final String captchaSolution, final Serializable saltSource); + + /** + * Validates the user input to solve the captcha against the returned token + * + * @param tokenString the returned token generated with the original captcha + * @param saltSource the salt source used to generate the original captcha + * @param answer the given answer to be validated + * @return boolean of the validation result. + */ + public abstract boolean validateToken(final String tokenString, final Serializable saltSource, final String answer); +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaGenerator.java b/src/main/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaGenerator.java new file mode 100644 index 0000000..73f07d2 --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaGenerator.java @@ -0,0 +1,30 @@ +package io.github.yaforster.flexcaptcha.core; + +import lombok.AllArgsConstructor; + +import java.io.Serializable; + +@AllArgsConstructor +public abstract class AbstractCaptchaGenerator { + + protected AbstractCaptchaCipher captchaCipher; + protected AbstractCaptchaRenderer renderer; + + /** + * @param solution predefined string solution from which the image and the token are generated + * @param saltSource Object used during creation of the captcha token to ensure authenticity + * @return Captcha object containing the image data of the visual captcha and the token + */ + public abstract Captcha generate(final String solution, final Serializable saltSource); + + /** + * Validates the token from a previous captcha generation call against the given user input and a serializable + * object used when the token was originally created. + * + * @param token token generated from a previous call to the generate method. + * @param userInput answer given by the user read from the rendered image + * @param saltSource Object used during creation of the captcha token to ensure authenticity + * @return true if the validation was successful + */ + public abstract boolean validate(final String token, final String userInput, final Serializable saltSource); +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaImageBackground.java b/src/main/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaImageBackground.java new file mode 100644 index 0000000..91d45ae --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaImageBackground.java @@ -0,0 +1,14 @@ +package io.github.yaforster.flexcaptcha.core; + +import java.awt.image.BufferedImage; + +public abstract class AbstractCaptchaImageBackground { + + /** + * Draws the background into the given {@link BufferedImage} of the captcha image. + * + * @param captchaImage {@link BufferedImage} to draw the background of. + */ + public abstract void drawBackground(final BufferedImage captchaImage); + +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaRenderer.java b/src/main/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaRenderer.java new file mode 100644 index 0000000..7465f5b --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaRenderer.java @@ -0,0 +1,40 @@ +package io.github.yaforster.flexcaptcha.core; + +import lombok.AllArgsConstructor; + +import java.awt.*; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + +@AllArgsConstructor +public abstract class AbstractCaptchaRenderer { + + protected int pictureHeight; + protected int pictureWidth; + protected List availableTextColors; + protected String imgFileFormat; + + /** + * Generates the visual representation of the captcha and return it as array of bytes. + * + * @param textToRender String respresentation of the captcha solution that should be visible in the rendering + * result. + * @return byte array containing the finished rendering result. + */ + public abstract byte[] renderAndConvertToBytes(final String textToRender); + + /** + * Picks a random color from the array of possible text colors + * + * @param colors Color array from which to pick a random element. + * @return Color object, picked randomly out of the textCols-Field array + */ + protected final Color pickRandomColor(List colors) { + if (colors.size() == 1) { + return colors.get(0); + } + ThreadLocalRandom r = ThreadLocalRandom.current(); + int i = r.nextInt(colors.size()); + return colors.get(i); + } +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/core/Captcha.java b/src/main/java/io/github/yaforster/flexcaptcha/core/Captcha.java new file mode 100644 index 0000000..a70f372 --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/core/Captcha.java @@ -0,0 +1,23 @@ +package io.github.yaforster.flexcaptcha.core; + +import java.util.Base64; + +/** + * Object representing a captcha output consisting of the image data to display and transport the + * actual captcha, and a token representing the hashed and salted solution + * + * @param token String containing the hash of the original solution salted with a specified object + * @param imgData String representation of the image containing the visual captcha + * @author Yannick Forster + */ +public record Captcha(String token, byte[] imgData) { + + /** + * Returns the image data byte array as base64 string. + * + * @return String of the base64-encoded imgData byte-array + */ + public String getImgDataAsBase64() { + return Base64.getEncoder().encodeToString(imgData); + } +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/core/CaptchaGenerator.java b/src/main/java/io/github/yaforster/flexcaptcha/core/CaptchaGenerator.java new file mode 100644 index 0000000..b7b3e56 --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/core/CaptchaGenerator.java @@ -0,0 +1,22 @@ +package io.github.yaforster.flexcaptcha.core; + +import java.io.Serializable; + +public class CaptchaGenerator extends AbstractCaptchaGenerator { + + public CaptchaGenerator(AbstractCaptchaCipher captchaCipher, AbstractCaptchaRenderer renderer) { + super(captchaCipher, renderer); + } + + @Override + public final Captcha generate(final String solution, final Serializable saltSource) { + String token = captchaCipher.generateToken(solution, saltSource); + byte[] imgBytes = renderer.renderAndConvertToBytes(solution); + return new Captcha(token, imgBytes); + } + + @Override + public final boolean validate(String token, String userInput, Serializable saltSource) { + return captchaCipher.validateToken(token, saltSource, userInput); + } +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/imgbased/ImageCaptcha.java b/src/main/java/io/github/yaforster/flexcaptcha/imgbased/ImageCaptcha.java deleted file mode 100644 index 11cb34b..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/imgbased/ImageCaptcha.java +++ /dev/null @@ -1,34 +0,0 @@ -package io.github.yaforster.flexcaptcha.imgbased; - -import io.github.yaforster.flexcaptcha.Captcha; -import lombok.Getter; -import lombok.Setter; - -/** - * Object representing an image-based captcha output consisting of the image - * data of the images in the grid to display and transport the actual captcha, - * and a token representing the hashed and salted solution - * - * @author Yannick Forster - */ -@Getter -@Setter -public class ImageCaptcha extends Captcha { - - /** - * array of byte arrays of the images of the captcha - */ - private byte[][] imgData; - - /** - * Creates a new image captcha object and sets its array of image data and the - * token - * - * @param imgData an array of byte arrays for the image data - * @param token the token - */ - public ImageCaptcha(byte[][] imgData, String token) { - super(token); - this.imgData = imgData.clone(); - } -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/imgbased/ImageLoader.java b/src/main/java/io/github/yaforster/flexcaptcha/imgbased/ImageLoader.java deleted file mode 100644 index cb199fe..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/imgbased/ImageLoader.java +++ /dev/null @@ -1,80 +0,0 @@ -package io.github.yaforster.flexcaptcha.imgbased; - -import lombok.Getter; -import lombok.Setter; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.File; -import java.io.FilenameFilter; -import java.io.IOException; -import java.nio.file.NotDirectoryException; -import java.util.Objects; -import java.util.stream.Stream; - -/** - * Helps bulk loading of image files from a given directory. File formats can be - * specified. - * - * @author Yannick Forster - */ -@Getter -@Setter -public class ImageLoader { - - /** - * Log4J Logger - */ - private Logger log = LogManager.getLogger(ImageLoader.class); - - /** - * The supported image file types. Will filter out all files that do not have - * these extensions when loading files. - */ - private String[] EXTENSIONS = new String[]{"gif", "png", "bmp", "jpg", "jpeg"}; - - /** - * Gets all images with the extensions defined in the EXTENSIONS field as - * {@link BufferedImage}s - * - * @param dirPath Directory path from which to load - * @return Array of {@link BufferedImage}s loaded from the directory - * @throws NotDirectoryException if the specified Path is no directory. - */ - public final BufferedImage[] getImagesfromPath(String dirPath) throws NotDirectoryException { - File dir = new File(dirPath); - if (!dir.isDirectory()) { - throw new NotDirectoryException(dirPath); - } - FilenameFilter filter = createNewFileFilter(); - File[] imgFiles = dir.listFiles(filter); - return Stream.of(Objects.requireNonNull(imgFiles)).map(file -> { - try { - return ImageIO.read(file); - } catch (IOException e) { - log.error("Error loading image:" + e.getMessage()); - return null; - } - }).filter(Objects::nonNull).toArray(BufferedImage[]::new); - } - - /** - * Creates a {@link FilenameFilter} allowing files ending with file name - * extensions defined in the EXTENSIONS field - * - * @return configured {@link FilenameFilter} - */ - private FilenameFilter createNewFileFilter() { - return (dir, name) -> { - for (final String ext : EXTENSIONS) { - if (name.endsWith("." + ext)) { - return (true); - } - } - return (false); - }; - } - -} \ No newline at end of file diff --git a/src/main/java/io/github/yaforster/flexcaptcha/imgbased/handling/ImageCaptchaHandler.java b/src/main/java/io/github/yaforster/flexcaptcha/imgbased/handling/ImageCaptchaHandler.java deleted file mode 100644 index acedbbe..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/imgbased/handling/ImageCaptchaHandler.java +++ /dev/null @@ -1,125 +0,0 @@ -package io.github.yaforster.flexcaptcha.imgbased.handling; - -import io.github.yaforster.flexcaptcha.CaptchaHandler; -import io.github.yaforster.flexcaptcha.CipherHandler; -import io.github.yaforster.flexcaptcha.imgbased.ImageCaptcha; -import org.apache.commons.lang3.ArrayUtils; - -import java.awt.image.BufferedImage; -import java.io.Serializable; -import java.util.Comparator; -import java.util.Optional; -import java.util.stream.Stream; - -/** - * Interface for defining the methods for image captcha handlers besides - * offering default logic for resizing the images from the input, to allow for - * clean display in a grid - * - * @author Yannick Forster - */ -public interface ImageCaptchaHandler extends CaptchaHandler { - - /** - * Gets the largest height out of all the {@link BufferedImage}s - * - * @param allImages array of {@link BufferedImage}s to check - * @return int the height of the image with the largest height - */ - private static int getLargestHeight(BufferedImage[] allImages) { - Optional greatestHeight = Stream.of(allImages) - .max(Comparator.comparing(BufferedImage::getHeight)); - return greatestHeight.map(BufferedImage::getHeight).orElse(Optional.of(allImages[0].getWidth()).orElse(100)); - /* Fallback in case comparing the images does not work. */ - } - - /** - * Gets the largest width out of all the {@link BufferedImage}s - * - * @param allImages array of {@link BufferedImage}s to check - * @return int the width of the image with the largest width - */ - private static int getLargestWidth(BufferedImage[] allImages) { - Optional greatestWidth = Stream.of(allImages).max(Comparator.comparing(BufferedImage::getWidth)); - return greatestWidth.map(BufferedImage::getWidth).orElse(Optional.of(allImages[0].getWidth()).orElse(100)); - /* Fallback in case comparing the images does not work. */ - } - - /** - * /** Generates an image-based captcha, forming a square-shaped grid with the - * height being the same as the grid width. The captcha will contain the byte - * data of the pictures in the grid, with the token being formed from the - * positions of the images that were taken from the solutionImages-Array while - * all other positions are filled with other images. This method determines the - * largest height and width of all the loaded images (both in solutionImages and - * otherImages) and uses those values to resize every image to achieve uniform - * dimensions. - * - * @param gridWidth The width of the grid of images. The grid is square - * shaped, so a size of 3 will result in 9 cells making up - * a grid of 3x3. - * @param cipherHandler {@link CipherHandler} object used to handle the - * encryption of the token itself and the self reference - * part inside the token - * @param saltSource A {@link Serializable} used to salt the token. - * @param password the password used to encrypt the implementation - * reference - * @param solutionImages Array of {@link BufferedImage}s used as the correct - * images in the grid - * @param fillImages Array of {@link BufferedImage}s used as the wrong - * images in the grid, filling the grid at every position - * not containing an image from the solutionImages array. - * @param addSelfReference boolean to control whether the reference to the encrypting - * class should be included in the encrypted value - * @return {@link ImageCaptcha} object containing the hashed solution and the - * grid as array of byte arrays. - */ - default ImageCaptcha generate(int gridWidth, CipherHandler cipherHandler, Serializable saltSource, - String password, BufferedImage[] solutionImages, BufferedImage[] fillImages, boolean addSelfReference) { - BufferedImage[] allImages = ArrayUtils.addAll(solutionImages, fillImages); - if (solutionImages == null || solutionImages.length == 0) { - throw new IllegalArgumentException("solutionImages can not be empty or null."); - } - if (fillImages == null || fillImages.length == 0) { - throw new IllegalArgumentException("fillImages can not be empty or null."); - } - int largestHeight = getLargestHeight(allImages); - int largestwidth = getLargestWidth(allImages); - return generate(gridWidth, cipherHandler, saltSource, password, solutionImages, fillImages, largestHeight, - largestwidth, addSelfReference); - } - - /** - * /** Generates an image-based captcha, forming a square-shaped grid with the - * height being the same as the grid width. The captcha will contain the byte - * data of the pictures in the grid, with the token being formed from the - * positions of the images that were taken from the solutionImages-Array while - * all other positions are filled with other images. - * - * @param gridWidth The width of the grid of images. The grid is square - * shaped, so a size of 3 will result in 9 cells making up - * a grid of 3x3. - * @param cipherHandler {@link CipherHandler} object used to handle the - * encryption of the token itself and the self reference - * part inside the token - * @param saltSource A {@link Serializable} used to salt the token. - * @param password the password used to encrypt the implementation - * reference - * @param solutionImages Array of {@link BufferedImage}s used as the correct - * images in the grid - * @param fillImages Array of {@link BufferedImage}s used as the wrong - * images in the grid, filling the grid at every position - * not containing an image from the solutionImages array. - * @param imageHeight the height to which every image is resized to fit the - * grid - * @param imageWidth the width to which every image is resized to fit the - * grid - * @param addSelfReference boolean to control whether the reference to the encrypting - * class should be included in the encrypted value - * @return {@link ImageCaptcha} object containing the hashed solution and the - * grid as array of byte arrays. - */ - ImageCaptcha generate(int gridWidth, CipherHandler cipherHandler, Serializable saltSource, String password, - BufferedImage[] solutionImages, BufferedImage[] fillImages, int imageHeight, int imageWidth, boolean addSelfReference); - -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/imgbased/handling/impl/SimpleImageCaptchaHandler.java b/src/main/java/io/github/yaforster/flexcaptcha/imgbased/handling/impl/SimpleImageCaptchaHandler.java deleted file mode 100644 index 1584e04..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/imgbased/handling/impl/SimpleImageCaptchaHandler.java +++ /dev/null @@ -1,183 +0,0 @@ -package io.github.yaforster.flexcaptcha.imgbased.handling.impl; - -import io.github.yaforster.flexcaptcha.CipherHandler; -import io.github.yaforster.flexcaptcha.imgbased.ImageCaptcha; -import io.github.yaforster.flexcaptcha.imgbased.handling.ImageCaptchaHandler; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.logging.log4j.LogManager; -import org.apache.logging.log4j.Logger; - -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.Serializable; -import java.util.Arrays; -import java.util.Objects; -import java.util.concurrent.ThreadLocalRandom; -import java.util.stream.IntStream; -import java.util.stream.Stream; - -/** - * Provides basic captcha handling regarding generation of a simplistic visual - * representation of the image-based captcha string as well as hashing the token - * and salt object - * - * @author Yannick Forster - */ - -public class SimpleImageCaptchaHandler implements ImageCaptchaHandler { - - /** - * The image format - */ - private static final String IMG_FORMAT = "PNG"; - /** - * Log4J Logger - */ - private final Logger log = LogManager.getLogger(SimpleImageCaptchaHandler.class); - - /** - * Resizes all images to the height and width specified - * - * @param allImages The {@link BufferedImage}s to resize - * @param height the target height to resize - * @param width the target width to resize - * @return array of resized {@link BufferedImage}s - */ - private static BufferedImage[] resizeImages(BufferedImage[] allImages, int height, int width) { - return Stream.of(allImages).map(img -> { - Image imageObj = img.getScaledInstance(width, height, img.getType()); - BufferedImage dimg = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); - Graphics2D g2d = dimg.createGraphics(); - g2d.drawImage(imageObj, 0, 0, null); - g2d.dispose(); - return dimg; - }).toArray(BufferedImage[]::new); - } - - /** - * Generates the image captcha. Randomizes a grid layout with the images taken - * from solutionImages and otherImages - */ - public ImageCaptcha generate(int gridWidth, CipherHandler cipherHandler, Serializable saltSource, String password, - BufferedImage[] solutionImages, BufferedImage[] fillImages, int height, int width, boolean addSelfReference) { - if (solutionImages == null || solutionImages.length == 0) { - throw new IllegalArgumentException("solutionImages can not be empty or null."); - } - if (fillImages == null || fillImages.length == 0) { - throw new IllegalArgumentException("fillImages can not be empty or null."); - } - if (gridWidth <= 1) { - throw new IllegalArgumentException("The gridWidth must be larger than 1."); - } - if (height <= 0) { - throw new IllegalArgumentException("The height must be an integer larger than 0."); - } - if (width <= 0) { - throw new IllegalArgumentException("The width must be an integer larger than 0."); - } - solutionImages = resizeImages(solutionImages, height, width); - fillImages = resizeImages(fillImages, height, width); - byte[][] gridData = new byte[gridWidth * gridWidth][]; - int halfGrid = Double.valueOf(Math.ceil(gridData.length / 2f)).intValue(); - int[] gridIndices = IntStream.range(0, gridData.length).boxed().mapToInt(Integer::intValue).toArray(); - ArrayUtils.shuffle(gridIndices); - int[] solutionIndices = Arrays.copyOfRange(gridIndices, 0, halfGrid); - Arrays.sort(solutionIndices); - int[] fillIndices = ArrayUtils.removeElements(gridIndices, solutionIndices); - return makeImageCaptcha(saltSource, cipherHandler, password, solutionImages, fillImages, gridData, solutionIndices, - fillIndices, addSelfReference); - } - - /** - * Checks if the given answer is correct - */ - @Override - public boolean validate(String answer, String token, CipherHandler cipherHandler, Serializable saltSource, String password) { - return token.split(DELIMITER)[0].equals(makeToken(answer, saltSource)); - } - - /** - * Generates the completed captcha object with a picture grid based on the - * specified gridsize, solution pictures and fill pictures. The salt source - * specifies an arbitrary object used to salt the token. - * - * @param saltSource arbitrary object to be used to salt the solution hash - * for added security and to allow for authenticating the - * given answer - * @param password the password used to encrypt the implementation - * reference - * @param solutionImages the "correct" images that the user is supposed to - * select - * @param fillImages all images that are not the solution - * @param gridData Array of byte arrays which is filled with the images - * or null if one of the images could not be loaded into - * the grid. - * @param solutionIndices the indices to fill with correct images - * @param fillIndices the indices to fill with filler images - * @return {@link ImageCaptcha} containing the finalized captcha - */ - private ImageCaptcha makeImageCaptcha(Serializable saltSource, CipherHandler cipherHandler, String password, BufferedImage[] solutionImages, - BufferedImage[] fillImages, byte[][] gridData, int[] solutionIndices, int[] fillIndices, boolean addSelfReference) { - gridData = fillGridWithImages(gridData, solutionImages, solutionIndices); - gridData = fillGridWithImages(gridData, fillImages, fillIndices); - if (gridData == null || Stream.of(gridData).anyMatch(Objects::isNull)) { - return null; - } - String token = generateToken(cipherHandler, saltSource, password, solutionIndices, addSelfReference); - return new ImageCaptcha(gridData, token); - } - - /** - * Iterates over all indices and puts the byte data of a random image picked in - * the corresponding element of gridData. - * - * @param gridData Array of byte arrays holding the image data - * @param imagesToAdd Array of images representing a pool from which the - * gridData is populated. - * @param indicesToFill The indices into which images from the imagesToAdd-Array - * are filled - * @return Array of byte arrays which is filled with the images or null if one - * of the images could not be loaded into the grid. - */ - private byte[][] fillGridWithImages(byte[][] gridData, BufferedImage[] imagesToAdd, int[] indicesToFill) { - if (gridData != null) { - IntStream.of(indicesToFill).forEach(i -> { - try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { - int rndIndex = ThreadLocalRandom.current().nextInt(imagesToAdd.length); - BufferedImage solutionImg = imagesToAdd[rndIndex]; - ImageIO.write(solutionImg, IMG_FORMAT, bos); - byte[] bytes = bos.toByteArray(); - gridData[i] = bytes; - } catch (IOException e) { - log.fatal("Could not write loaded image to byte array during grid filling. " + e.getMessage()); - } - }); - } - return gridData; - } - - /** - * Generates the token from the saltsource and the indices of the solution (the - * correct ones) - * - * @param saltSource Object used during creation of the captcha token to - * ensure authenticity - * @param password the password used to encrypt the implementation - * reference - * @param solutionIndices the correct indices in the captcha - * @return String of the token - */ - private String generateToken(CipherHandler cipherHandler, Serializable saltSource, String password, int[] solutionIndices, boolean addSelfReference) { - Arrays.sort(solutionIndices); - String solution = Arrays.toString(solutionIndices).replaceAll("\\s+", ""); - String token = makeToken(solution, saltSource); - if (addSelfReference) { - token += addSelfReference(cipherHandler, saltSource, password); - } - return token; - } - -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/BackgroundImage.java b/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/BackgroundImage.java new file mode 100644 index 0000000..d0878da --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/BackgroundImage.java @@ -0,0 +1,24 @@ +package io.github.yaforster.flexcaptcha.impl.rendering; + +import lombok.Getter; + +import java.awt.*; +import java.awt.image.BufferedImage; + +@Getter +public class BackgroundImage extends FlatColorBackground { + + private final BufferedImage backgroundImage; + + public BackgroundImage(Color backgroundImageColor, BufferedImage backgroundImage) { + super(backgroundImageColor); + this.backgroundImage = backgroundImage; + } + + @Override + public void drawBackground(final BufferedImage captchaImage) { + super.drawBackground(captchaImage); + Graphics2D captchaImageGraphic = captchaImage.createGraphics(); + captchaImageGraphic.drawImage(backgroundImage, null, 0, 0); + } +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/CaptchaRenderer.java b/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/CaptchaRenderer.java new file mode 100644 index 0000000..78ce5d2 --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/CaptchaRenderer.java @@ -0,0 +1,261 @@ +package io.github.yaforster.flexcaptcha.impl.rendering; + +import com.jhlabs.image.AbstractBufferedImageOp; +import io.github.yaforster.flexcaptcha.core.AbstractCaptchaImageBackground; +import io.github.yaforster.flexcaptcha.core.AbstractCaptchaRenderer; +import lombok.Builder; +import org.apache.commons.lang3.StringUtils; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.geom.AffineTransform; +import java.awt.image.BufferedImage; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.security.SecureRandom; +import java.util.Collections; +import java.util.List; +import java.util.Optional; +import java.util.stream.IntStream; + +public class CaptchaRenderer extends AbstractCaptchaRenderer { + + private static final int DEFAULT_PICTURE_HEIGHT = 100; + private static final int DEFAULT_PICTURE_WIDTH = 300; + private static final String DEFAULT_IMAGE_FORMAT = "JPG"; + /** + * Default value for the background of the image. Renders as plain white background with no texture. + */ + private static final AbstractCaptchaImageBackground DEFAULT_BACKGROUND = new FlatColorBackground(Color.white); + /** + * Default value of the upper bound of randomized angle any rendered character can have + */ + private static final double DEFAULT_MAX_LETTER_ROTATION_ANGLE = 0.35d; + private static final String DEFAULT_FONT = "Verdana"; + /** + * Defines the maximum angle that can be used to rotate a single character in the captcha + */ + private final Double maxrotateAngle; + /** + * String name of the font to be used when writing characters to the image + */ + private final String fontName; + /** + * List of operations that will be applied to the image during rendering. + */ + private final List imageOperationsList; + /** + * Settings for how the Background of the Captcha is supposed to be rendered. + */ + private final AbstractCaptchaImageBackground imageBackground; + /** + * Settings for the noise added to the image. + */ + private final NoiseSettings noiseSettings; + + @Builder + public CaptchaRenderer(int pictureHeight, int pictureWidth, List availableTextColors, String imgFileFormat + , Double maxrotateAngle, String fontName, List imageOperationsList, + AbstractCaptchaImageBackground imageBackground, NoiseSettings noiseSettings) { + super(getPictureHeightOrDefault(pictureHeight), getPictureWidthOrDefault(pictureWidth), + (availableTextColors == null || availableTextColors.isEmpty()) ? + Collections.singletonList(Color.BLACK) : availableTextColors, + StringUtils.isBlank(imgFileFormat) ? DEFAULT_IMAGE_FORMAT : imgFileFormat); + this.maxrotateAngle = Optional.ofNullable(maxrotateAngle).orElse(DEFAULT_MAX_LETTER_ROTATION_ANGLE); + this.fontName = StringUtils.isBlank(fontName) ? DEFAULT_FONT : fontName; + this.imageOperationsList = Optional.ofNullable(imageOperationsList).orElse(Collections.emptyList()); + this.imageBackground = Optional.ofNullable(imageBackground).orElse(DEFAULT_BACKGROUND); + this.noiseSettings = getNoiseSettingsOrDefault(noiseSettings); + } + + /** + * Checks the given pictureHeight and uses the default instead if the value is 0 + */ + private static int getPictureHeightOrDefault(int pictureHeight) { + return pictureHeight != 0 ? pictureHeight : DEFAULT_PICTURE_HEIGHT; + } + + /** + * Checks the given pictureWidth and uses the default instead if the value is 0 + */ + private static int getPictureWidthOrDefault(int pictureWidth) { + return pictureWidth != 0 ? pictureWidth : DEFAULT_PICTURE_WIDTH; + } + + /** + * Sets a clean NoiseSetting that will not produce any noise when rendered if the given noiseSetting is null or + * contains no color. + */ + private static NoiseSettings getNoiseSettingsOrDefault(NoiseSettings noiseSettings) { + if (noiseSettings == null || noiseSettings.distortionsColor() == null) { + return null; + } + else { + return noiseSettings; + } + } + + /** + * @return Shortcut to get a default renderer without any user customized settings + */ + public static CaptchaRenderer getDefaultCaptchaRenderer() { + return CaptchaRenderer.builder().build(); + } + + @Override + public final byte[] renderAndConvertToBytes(String textToRender) { + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + BufferedImage image = new BufferedImage(pictureWidth, pictureHeight, BufferedImage.TYPE_INT_RGB); + Graphics2D graphic = image.createGraphics(); + imageBackground.drawBackground(image); + if (noiseSettings != null) { + drawDistortions(graphic); + } + drawText(textToRender, image); + graphic.dispose(); + applyFilters(image); + ImageIO.write(image, imgFileFormat, bos); + return bos.toByteArray(); + } + catch (IOException e) { + throw new CaptchaRenderingException(e); + } + } + + /** + * Draws distortions onto the given Graphics2D object in the shape of randomly + * generated rectangles and dots to help obscure the text in the captcha image + * + * @param graphic the Graphics2D object onto which the distortions are to be + * drawn + */ + private void drawDistortions(Graphics2D graphic) { + int lineCount = (pictureWidth * noiseSettings.noiseIntensity()) / 100; + int dotCount = ((pictureHeight * pictureWidth) * noiseSettings.noiseIntensity()) / 100; + for (int i = 0; i < lineCount; i++) { + drawRectangleNoise(graphic); + } + for (int j = 0; j < dotCount; j++) { + drawLineNoise(graphic); + } + } + + /** + * prepares the writing of the given captcha text onto the specified Graphics2d + * object + * + * @param textToRender string containing the text to write + * @param image the image on which to draw the text + */ + private void drawText(String textToRender, BufferedImage image) { + int chars = textToRender.length(); + TextRenderingData textRenderingData = buildTextRenderingData(image, chars); + IntStream.range(0, chars).boxed().forEachOrdered(i -> { + char charToDraw = textToRender.charAt(i); + drawCharacter(image, textRenderingData, i, charToDraw); + }); + } + + /** + * Applies the stored operations in imageOperationsList to the image. + * + * @param image the image on which to render + */ + private void applyFilters(BufferedImage image) { + imageOperationsList.stream().forEachOrdered(op -> op.filter(image, image)); + } + + private void drawRectangleNoise(Graphics2D graphic) { + graphic.setColor(noiseSettings.distortionsColor()); + SecureRandom rnd = new SecureRandom(); + int L = (int) (rnd.nextDouble() * pictureHeight / 2.0); + int X = (int) (rnd.nextDouble() * pictureWidth - L); + int Y = (int) (rnd.nextDouble() * pictureHeight - L); + graphic.draw3DRect(X, Y, L << 1, L << 1, true); + } + + private void drawLineNoise(Graphics2D graphic) { + Color darkerBackgrnd = noiseSettings.distortionsColor().darker(); + graphic.setColor(darkerBackgrnd); + SecureRandom rnd = new SecureRandom(); + int x = Math.abs(rnd.nextInt()) % pictureWidth; + int y = Math.abs(rnd.nextInt()) % pictureHeight; + graphic.drawLine(x, y, x, y); + } + + /** + * Builds a {@link TextRenderingData} object for cleaner passing of multiple arguments within this class + * + * @param image the image on which to draw the text + * @return {@link TextRenderingData} with text rendering information derived from the given image and the number + * of characters + */ + private TextRenderingData buildTextRenderingData(BufferedImage image, int chars) { + Graphics2D graphic = image.createGraphics(); + Font textFont = new Font(fontName, Font.BOLD, (int) (image.getHeight() / 2.5)); + graphic.setColor(pickRandomColor(availableTextColors)); + graphic.setFont(textFont); + FontMetrics fontMetrics = graphic.getFontMetrics(); + int maxAdvance = fontMetrics.getMaxAdvance(); + int fontHeight = fontMetrics.getHeight(); + int charDim = Math.max(maxAdvance, fontHeight); + int margin = image.getWidth() / 16; + float spaceForLetters = (-margin << 1) + image.getWidth(); + float spaceBetweenCharacters = spaceForLetters / (chars - 1.0f); + return new TextRenderingData(textFont, fontMetrics, margin, spaceBetweenCharacters, maxAdvance, fontHeight, + charDim); + } + + /** + * Measures the font and draws each character of the given string to the + * Graphics2D object at a randomized angle. + * + * @param tRD Text rendering data used to bundle all relevant data about the font used to compute their + * placement within the captcha image. + * @param image the image on which to draw the text + * @param charToDraw the individual character to measure and draw + * @param index running index of the character in the source string + */ + private void drawCharacter(BufferedImage image, TextRenderingData tRD, Integer index, char charToDraw) { + BufferedImage charImage = getImageOfAngledRenderedCharacter(tRD, charToDraw); + int charDim = tRD.charDim(); + int x = getHorizontalPlacementOfCharacter(tRD, index, charDim); + int y = (image.getHeight() - charDim) / 2; + image.createGraphics().drawImage(charImage, x, y, charDim, charDim, null, null); + } + + /** + * @param tRD Text rendering data used to bundle all relevant data about the font used to compute their + * placement within the captcha image. + * @param charToDraw the individual character to measure and draw + * @return BufferedImage containing a single rendered and angled character to be merged with the main captcha image. + */ + private BufferedImage getImageOfAngledRenderedCharacter(TextRenderingData tRD, char charToDraw) { + int charDim = tRD.charDim(); + int charWidth = tRD.fontMetrics().charWidth(charToDraw); + int halfCharDim = charDim / 2; + double angle = getRandomAngleWithinMaximumBounds(); + int charX = (int) (0.5 * charDim - 0.5 * charWidth); + BufferedImage charImage = new BufferedImage(charDim, charDim, BufferedImage.TYPE_INT_ARGB); + Graphics2D charGraphics = charImage.createGraphics(); + charGraphics.translate(halfCharDim, halfCharDim); + charGraphics.transform(AffineTransform.getRotateInstance(angle)); + charGraphics.translate(-halfCharDim, -halfCharDim); + charGraphics.setColor(pickRandomColor(availableTextColors)); + charGraphics.setFont(tRD.font()); + charGraphics.drawString(String.valueOf(charToDraw), charX, (charDim - tRD.fontMetrics() + .getAscent()) / 2 + tRD.fontMetrics().getAscent()); + charGraphics.dispose(); + return charImage; + } + + private static int getHorizontalPlacementOfCharacter(TextRenderingData tRD, Integer index, int charDimensions) { + return (int) (tRD.margin() + tRD.spaceBetweenCharacters() * (index.floatValue()) - charDimensions / 2.0f); + } + + private double getRandomAngleWithinMaximumBounds() { + return (new SecureRandom().nextDouble() - 0.5) * maxrotateAngle; + } + + +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/CaptchaRenderingException.java b/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/CaptchaRenderingException.java new file mode 100644 index 0000000..80b22c4 --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/CaptchaRenderingException.java @@ -0,0 +1,7 @@ +package io.github.yaforster.flexcaptcha.impl.rendering; + +import lombok.experimental.StandardException; + +@StandardException +public class CaptchaRenderingException extends RuntimeException { +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/FlatColorBackground.java b/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/FlatColorBackground.java new file mode 100644 index 0000000..12043c6 --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/FlatColorBackground.java @@ -0,0 +1,25 @@ +package io.github.yaforster.flexcaptcha.impl.rendering; + +import io.github.yaforster.flexcaptcha.core.AbstractCaptchaImageBackground; +import lombok.AllArgsConstructor; +import lombok.Getter; + +import java.awt.*; +import java.awt.image.BufferedImage; + +@Getter +@AllArgsConstructor +public class FlatColorBackground extends AbstractCaptchaImageBackground { + + /** + * Color of the Captcha Background + */ + protected final Color backgroundColor; + + @Override + public void drawBackground(final BufferedImage captchaImage) { + Graphics captchaImageGraphic = captchaImage.getGraphics(); + captchaImageGraphic.setColor(backgroundColor); + captchaImageGraphic.fillRect(0, 0, captchaImage.getWidth(), captchaImage.getHeight()); + } +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/NoiseSettings.java b/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/NoiseSettings.java new file mode 100644 index 0000000..5cf4859 --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/NoiseSettings.java @@ -0,0 +1,6 @@ +package io.github.yaforster.flexcaptcha.impl.rendering; + +import java.awt.*; + +public record NoiseSettings(int noiseIntensity, Color distortionsColor) { +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/TextRenderingData.java b/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/TextRenderingData.java new file mode 100644 index 0000000..422805c --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/impl/rendering/TextRenderingData.java @@ -0,0 +1,7 @@ +package io.github.yaforster.flexcaptcha.impl.rendering; + +import java.awt.*; + +public record TextRenderingData(Font font, FontMetrics fontMetrics, int margin, float spaceBetweenCharacters, + int maxAdvance, int fontHeight, int charDim) { +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/impl/token/CaptchaCipher.java b/src/main/java/io/github/yaforster/flexcaptcha/impl/token/CaptchaCipher.java new file mode 100644 index 0000000..6afb9e1 --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/impl/token/CaptchaCipher.java @@ -0,0 +1,108 @@ +package io.github.yaforster.flexcaptcha.impl.token; + +import io.github.yaforster.flexcaptcha.core.AbstractCaptchaCipher; +import lombok.Builder; +import org.apache.commons.lang3.StringUtils; + +import javax.crypto.BadPaddingException; +import javax.crypto.Cipher; +import javax.crypto.IllegalBlockSizeException; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.Serializable; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.util.Arrays; +import java.util.Base64; +import java.util.Optional; + +public class CaptchaCipher extends AbstractCaptchaCipher { + + @Builder + private CaptchaCipher(CipherSettings cipherSettings, String encryptionPassword, + ExpirationTimeSettings expirationTimeSettings) { + super(Optional.ofNullable(cipherSettings) + .orElse(CipherSettings.getDefaultCipherSettings()), Optional.ofNullable(encryptionPassword) + .orElse(StringUtils.EMPTY), expirationTimeSettings); + } + + @Override + public final String generateToken(String captchaSolution, Serializable saltSource) { + try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) { + byte[] ivBytes = generateIV().getIV(); + Cipher cipher = getCipher(encryptionPassword, saltSource, Cipher.ENCRYPT_MODE, ivBytes); + byte[] cipherBytes = cipher.doFinal(captchaSolution.getBytes()); + outputStream.write(ivBytes); + if (expirationTimeSettings != null) { + appendExpirationDateBytes(cipher, outputStream); + } + outputStream.write(cipherBytes); + return Base64.getEncoder().encodeToString(outputStream.toByteArray()); + } + catch (Exception originalException) { + throw mapEncryptionException(originalException); + } + } + + private void appendExpirationDateBytes(Cipher cipher, ByteArrayOutputStream outputStream) throws IOException { + byte[] encryptedExpirationDateBytes = encryptExpirationDateBytes(cipher); + outputStream.write(encryptedExpirationDateBytes); + } + + private TokengenerationException mapEncryptionException(Exception originalException) { + String errorMessage = switch (originalException) { + case IllegalBlockSizeException ibse -> ibse.getLocalizedMessage(); + case BadPaddingException bpe -> + "Unknown padding specified for token encryption: " + bpe.getLocalizedMessage(); + default -> "Fatal error during cryptographic operation: " + originalException.getLocalizedMessage(); + }; + return new TokengenerationException(errorMessage, originalException); + } + + private byte[] encryptExpirationDateBytes(Cipher cipher) { + try { + long expirationTime = + expirationTimeSettings.expirationTimeMillisOffset() + expirationTimeSettings.getTime(); + byte[] expirationDateBytes = BigInteger.valueOf(expirationTime).toByteArray(); + return cipher.doFinal(expirationDateBytes); + } + catch (GeneralSecurityException originalException) { + throw mapEncryptionException(originalException); + } + } + + @Override + public final boolean validateToken(String tokenString, Serializable saltSource, String userAnswer) { + try { + int ivBytesEnd = 16; + int cipherBytesStartIndexInToken = 16; + byte[] tokenbytes = Base64.getDecoder().decode(tokenString.getBytes()); + byte[] ivBytes = Arrays.copyOfRange(tokenbytes, 0, ivBytesEnd); + Cipher cipher = getCipher(encryptionPassword, saltSource, Cipher.DECRYPT_MODE, ivBytes); + if (expirationTimeSettings != null) { + cipherBytesStartIndexInToken += 16; + if (isTokenExpired(tokenbytes, cipherBytesStartIndexInToken, cipher)) { + return false; + } + } + byte[] cipherBytes = Arrays.copyOfRange(tokenbytes, cipherBytesStartIndexInToken, tokenbytes.length); + byte[] decryptedBytes = cipher.doFinal(cipherBytes); + return userAnswer.equals(new String(decryptedBytes)); + } + catch (Exception originalException) { + throw mapEncryptionException(originalException); + } + } + + private boolean isTokenExpired(byte[] tokenbytes, int cipherBytesStartIndexInToken, Cipher cipher) throws IllegalBlockSizeException, BadPaddingException { + long expirationTime = getDecryptedExpirationTimeMillis(tokenbytes, cipherBytesStartIndexInToken, cipher); + long currentTimeMillis = expirationTimeSettings.getTime(); + return currentTimeMillis > expirationTime; + } + + private long getDecryptedExpirationTimeMillis(byte[] tokenbytes, int cipherBytesStartIndexInToken, Cipher cipher) throws IllegalBlockSizeException, BadPaddingException { + byte[] expirationTimeBytes = Arrays.copyOfRange(tokenbytes, 16, cipherBytesStartIndexInToken); + byte[] decryptedExpirationTimeBytes = cipher.doFinal(expirationTimeBytes); + return new BigInteger(decryptedExpirationTimeBytes).longValue(); + } +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/impl/token/CaptchaCipherBuilder.java b/src/main/java/io/github/yaforster/flexcaptcha/impl/token/CaptchaCipherBuilder.java new file mode 100644 index 0000000..f0cef89 --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/impl/token/CaptchaCipherBuilder.java @@ -0,0 +1,4 @@ +package io.github.yaforster.flexcaptcha.impl.token; + +public class CaptchaCipherBuilder { +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/impl/token/CipherInstantiationException.java b/src/main/java/io/github/yaforster/flexcaptcha/impl/token/CipherInstantiationException.java new file mode 100644 index 0000000..b1994d2 --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/impl/token/CipherInstantiationException.java @@ -0,0 +1,7 @@ +package io.github.yaforster.flexcaptcha.impl.token; + +import lombok.experimental.StandardException; + +@StandardException +public class CipherInstantiationException extends RuntimeException { +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/impl/token/CipherSettings.java b/src/main/java/io/github/yaforster/flexcaptcha/impl/token/CipherSettings.java new file mode 100644 index 0000000..9a0fe0e --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/impl/token/CipherSettings.java @@ -0,0 +1,11 @@ +package io.github.yaforster.flexcaptcha.impl.token; + +public record CipherSettings(String encryptionAlgorithm, String cipherAlgorithm, String secretKeySpecAlgorithm) { + public static CipherSettings getDefaultCipherSettings() { + final String DEFAULT_ENCRYPTION_ALGORITHM = "PBKDF2WithHmacSHA256"; + final String DEFAULT_CIPHER_ALGORITHM = "AES/CBC/PKCS5Padding"; + final String DEFAULT_SECRETKEYSPEC_ALGORITHM = "AES"; + return new CipherSettings(DEFAULT_ENCRYPTION_ALGORITHM, DEFAULT_CIPHER_ALGORITHM, + DEFAULT_SECRETKEYSPEC_ALGORITHM); + } +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/impl/token/ExpirationTimeSettings.java b/src/main/java/io/github/yaforster/flexcaptcha/impl/token/ExpirationTimeSettings.java new file mode 100644 index 0000000..bf2c4df --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/impl/token/ExpirationTimeSettings.java @@ -0,0 +1,9 @@ +package io.github.yaforster.flexcaptcha.impl.token; + +import java.util.function.Supplier; + +public record ExpirationTimeSettings(Long expirationTimeMillisOffset, Supplier currentTimeProvider) { + public long getTime() { + return currentTimeProvider.get(); + } +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/impl/token/TokengenerationException.java b/src/main/java/io/github/yaforster/flexcaptcha/impl/token/TokengenerationException.java new file mode 100644 index 0000000..4a46fb2 --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/impl/token/TokengenerationException.java @@ -0,0 +1,7 @@ +package io.github.yaforster.flexcaptcha.impl.token; + +import lombok.experimental.StandardException; + +@StandardException +public class TokengenerationException extends RuntimeException { +} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/textbased/TextCaptcha.java b/src/main/java/io/github/yaforster/flexcaptcha/textbased/TextCaptcha.java deleted file mode 100644 index d2e0d46..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/textbased/TextCaptcha.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased; - -import io.github.yaforster.flexcaptcha.Captcha; -import lombok.Getter; -import lombok.Setter; - -import java.util.Base64; - -/** - * Object representing a text-based captcha output consisting of the image data - * of the visualized text to display and transport the actual captcha, and a - * token representing the solution - * - * @author Yannick Forster - */ - -@Getter -@Setter -public class TextCaptcha extends Captcha { - - /** - * String representation of the image containing the visual captcha - */ - private byte[] imgData; - - /** - * Creates a new TextCaptcha object and adds the byte array containing the image - * data and the token - * - * @param imgData byte array for the image data - * @param token the token - */ - public TextCaptcha(byte[] imgData, String token) { - super(token); - this.imgData = imgData.clone(); - } - - /** - * Returns the image data byte array as base64 string. - * - * @return String of the base64-encoded imgData byte-array - */ - public final String getImgDataAsBase64() { - if (imgData == null) { - throw new IllegalStateException("Cannot convert empty image data to string."); - } - return Base64.getEncoder().encodeToString(imgData); - } - -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/textbased/handling/TextCaptchaHandler.java b/src/main/java/io/github/yaforster/flexcaptcha/textbased/handling/TextCaptchaHandler.java deleted file mode 100644 index 1360173..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/textbased/handling/TextCaptchaHandler.java +++ /dev/null @@ -1,144 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.handling; - -import io.github.yaforster.flexcaptcha.CaptchaHandler; -import io.github.yaforster.flexcaptcha.CipherHandler; -import io.github.yaforster.flexcaptcha.textbased.TextCaptcha; -import io.github.yaforster.flexcaptcha.textbased.enums.Case; -import io.github.yaforster.flexcaptcha.textbased.rendering.TextImageRenderer; -import io.github.yaforster.flexcaptcha.textbased.textgen.CaptchaTextGenerator; - -import javax.imageio.ImageIO; -import java.awt.image.BufferedImage; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.Serializable; - -/** - * Interface for the various ways in which a captcha could potentially be - * created. - * - * @author Yannick Forster - */ -public interface TextCaptchaHandler extends CaptchaHandler { - - /** - * Generates a captcha of a given character length and salts the hashed solution - * with the given object for checking authenticity during verification. Uses the - * given String as a source of all possible characters from which the captcha - * string is to be generated with mixed case. - * - * @param length specifies the length - * @param cipherHandler {@link CipherHandler} implementation for encryption and - * decryption - * @param saltSource Object used during creation of the captcha token to - * ensure authenticity - * @param password the password for encryption - * @param textgenerator a {@link CaptchaTextGenerator} implementation - * @param renderer a {@link TextImageRenderer} implementation handling the - * visualization of the text as image - * @param height the pixel height of the captcha image - * @param width the pixel width of the captcha image - * @param addSelfReference boolean to control whether the reference to the encrypting - * class should be included in the encrypted value - * @return Captcha object containing the image data of the visual captcha and - * the token containing the hashed and salted solution - */ - default TextCaptcha generate(int length, CipherHandler cipherHandler, Serializable saltSource, - String password, CaptchaTextGenerator textgenerator, TextImageRenderer renderer, int height, int width, boolean addSelfReference) { - return generate(length, cipherHandler, saltSource, password, textgenerator, Case.MIXEDCASE, renderer, height, - width, addSelfReference); - } - - /** - * Generates a captcha of a given character length and salts the hashed solution - * with the given object for checking authenticity during verification. Uses the - * given String as a source of all possible characters from which the captcha - * string is to be generated with the specified case. - * - * @param length specifies the length - * @param cipherHandler {@link CipherHandler} implementation for encryption and - * decryption - * @param saltSource Object used during creation of the captcha token to - * ensure authenticity - * @param password the password for encryption - * @param textgenerator a {@link CaptchaTextGenerator} implementation - * @param renderer a {@link TextImageRenderer} implementation handling the - * visualization of the text as image - * @param charCase a {@link Case} enum defining what letter case is allowed - * in the generation - * @param height the pixel height of the captcha image - * @param width the pixel width of the captcha image - * @param addSelfReference boolean to control whether the reference to the encrypting - * class should be included in the encrypted value - * @return Captcha object containing the image data of the visual captcha and - * the token containing the hashed and salted solution - */ - TextCaptcha generate(int length, CipherHandler cipherHandler, Serializable saltSource, String password, - CaptchaTextGenerator textgenerator, Case charCase, TextImageRenderer renderer, int height, int width, boolean addSelfReference); - - /** - * Generates a captcha from a given string and salt object - * - * @param captchaText predefined string from which the image and the token are - * generated - * @param cipherHandler {@link CipherHandler} implementation for encryption and - * decryption - * @param saltSource Object used during creation of the captcha token to - * ensure authenticity - * @param password the password used to encrypt the implementation - * reference - * @param renderer a {@link TextImageRenderer} implementation handling the - * visualization of the text as image - * @param height the pixel height of the captcha image - * @param width the pixel width of the captcha image - * @param addSelfReference boolean to control whether the reference to the encrypting - * class should be included in the encrypted value - * @return Captcha object containing the image data of the visual captcha and - * the token containing the hashed and salted solution - */ - TextCaptcha toCaptcha(String captchaText, CipherHandler cipherHandler, Serializable saltSource, - String password, TextImageRenderer renderer, int height, int width, boolean addSelfReference); - - /** - * Converts a buffered image to a byte array - * - * @param image Image of the captcha - * @param imgFormat the image format in which the raw image data is written - * @return byte array of the image - */ - default byte[] convertImageToByteArray(BufferedImage image, String imgFormat) { - try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { - ImageIO.write(image, imgFormat, bos); - return bos.toByteArray(); - } catch (final IOException e) { - log.error("Error converting the BufferedImage to byte array: " + e.getMessage()); - return null; - } - } - - /** - * @param length the length of the Captcha - * @param textgenerator the given {@link CaptchaTextGenerator} to generate the captcha text - * @param renderer the given {@link TextImageRenderer} to generate the captcha image - * @param height the height of the image to be generated by the renderer - * @param width the width of the image to be generated by the renderer - */ - default void checkInputs(int length, CaptchaTextGenerator textgenerator, TextImageRenderer renderer, int height, int width) { - if (renderer == null) { - throw new IllegalArgumentException("The renderer cannot be null."); - } - if (textgenerator == null) { - throw new IllegalArgumentException("The text generator cannot be null."); - } - if (length <= 0) { - throw new IllegalArgumentException("The length must be an integer larger than 0."); - } - if (height <= 2) { - throw new IllegalArgumentException("The height must be an integer larger than 2."); - } - if (width <= 0) { - throw new IllegalArgumentException("The width must be an integer larger than 0."); - } - } - -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/textbased/handling/impl/SecureTextCaptchaHandler.java b/src/main/java/io/github/yaforster/flexcaptcha/textbased/handling/impl/SecureTextCaptchaHandler.java deleted file mode 100644 index d234fe1..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/textbased/handling/impl/SecureTextCaptchaHandler.java +++ /dev/null @@ -1,73 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.handling.impl; - -import io.github.yaforster.flexcaptcha.CipherHandler; -import io.github.yaforster.flexcaptcha.textbased.TextCaptcha; -import io.github.yaforster.flexcaptcha.textbased.enums.Case; -import io.github.yaforster.flexcaptcha.textbased.handling.TextCaptchaHandler; -import io.github.yaforster.flexcaptcha.textbased.rendering.TextImageRenderer; -import io.github.yaforster.flexcaptcha.textbased.textgen.CaptchaTextGenerator; -import org.apache.commons.lang3.StringUtils; - -import java.awt.image.BufferedImage; -import java.io.Serializable; -import java.util.Base64; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - -/** - * Provides basic captcha handling regarding the generation of a simplistic visual - * representation of the text-based captcha string as well as encrypting the - * token and salt object - * - * @author Yannick Forster - */ -public class SecureTextCaptchaHandler implements TextCaptchaHandler { - - /** - * The image format - */ - private static final String IMG_FORMAT = "JPEG"; - - @Override - public TextCaptcha generate(int length, CipherHandler cipherHandler, Serializable saltSource, String password, - CaptchaTextGenerator textGenerator, Case charCase, TextImageRenderer renderer, int height, int width, boolean addSelfReference) { - checkInputs(length, textGenerator, renderer, height, width); - String captchaText = textGenerator.generate(length, textGenerator.generate(length, charCase), charCase); - return toCaptcha(captchaText, cipherHandler, saltSource, password, renderer, height, width, addSelfReference); - } - - public TextCaptcha toCaptcha(String captchaText, CipherHandler cipherHandler, Serializable saltSource, - String password, TextImageRenderer renderer, int height, int width, boolean addSelfReference) { - TextCaptcha captcha = null; - BufferedImage image = renderer.render(captchaText, height, width); - try { - byte[] imgData = convertImageToByteArray(image, IMG_FORMAT); - CompletableFuture selfreference = CompletableFuture.completedFuture(StringUtils.EMPTY); - if (addSelfReference) { - selfreference = CompletableFuture.supplyAsync(() -> addSelfReference(cipherHandler, saltSource, password)); - } - CompletableFuture encryptedToken = CompletableFuture.supplyAsync(() -> cipherHandler.encryptString(captchaText.getBytes(), password, saltSource)); - String tokenString = Base64.getEncoder().encodeToString(encryptedToken.get()); - captcha = new TextCaptcha(imgData, tokenString + selfreference.get()); - } catch (InterruptedException e) { - log.fatal("Thread interruption during captcha generation: " + e.getLocalizedMessage()); - - } catch (ExecutionException e) { - log.fatal("Fatal error during captcha generation: " + e.getLocalizedMessage()); - } - return captcha; - } - - @Override - public boolean validate(String answer, String token, CipherHandler cipherHandler, Serializable saltSource, - String password) { - byte[] decoded = Base64.getDecoder().decode(token); - byte[] decryptedToken = cipherHandler.decryptString(decoded, password, saltSource); - if (decryptedToken != null) { - return answer.equals(new String(decryptedToken)); - } else { - return false; - } - } - -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/textbased/handling/impl/SimpleTextCaptchaHandler.java b/src/main/java/io/github/yaforster/flexcaptcha/textbased/handling/impl/SimpleTextCaptchaHandler.java deleted file mode 100644 index 2b15159..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/textbased/handling/impl/SimpleTextCaptchaHandler.java +++ /dev/null @@ -1,101 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.handling.impl; - -import io.github.yaforster.flexcaptcha.CipherHandler; -import io.github.yaforster.flexcaptcha.textbased.TextCaptcha; -import io.github.yaforster.flexcaptcha.textbased.enums.Case; -import io.github.yaforster.flexcaptcha.textbased.handling.TextCaptchaHandler; -import io.github.yaforster.flexcaptcha.textbased.rendering.TextImageRenderer; -import io.github.yaforster.flexcaptcha.textbased.textgen.CaptchaTextGenerator; -import org.apache.commons.lang3.StringUtils; - -import java.awt.image.BufferedImage; -import java.io.Serializable; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - -/** - * Provides basic captcha handling regarding generation of a simplistic visual - * representation of the text-based captcha string as well as hashing the token - * and salt object - * - * @author Yannick Forster - */ -public class SimpleTextCaptchaHandler implements TextCaptchaHandler { - - /** - * The image format - */ - private static final String IMG_FORMAT = "JPEG"; - - /** - * Generates a TextCaptcha object containing the token and the images. - */ - @Override - public TextCaptcha generate(int length, CipherHandler cipherHandler, Serializable saltSource, String password, - CaptchaTextGenerator textgenerator, Case charCase, TextImageRenderer renderer, int height, int width, boolean addSelfReference) { - checkInputs(length, textgenerator, renderer, height, width); - String captchaText = textgenerator.generate(length, textgenerator.generate(length, charCase), charCase); - return makeTextCaptcha(saltSource, cipherHandler, password, renderer, height, width, captchaText, addSelfReference); - } - - /** - * Validates whether the answer to the given captcha is correct. To do - * this, the answer and the salt source are combined and checked against the - * token - */ - @Override - public boolean validate(String answer, String token, CipherHandler cipherHandler, Serializable saltSource, String password) { - return token.split(DELIMITER)[0].equals(makeToken(answer, saltSource)); - } - - /** - * Generates the completed captcha object with a picture based on the specified - * text directly, The implementation of creating the image is given by the - * specified renderer. The salt source specifies an arbitrary object used to - * salt the token. Use this method if you want a captcha knowing the solution - * beforehand, as opposed to have it randomly generated. - */ - public TextCaptcha toCaptcha(String captchaText, CipherHandler cipherHandler, Serializable saltSource, String password, - TextImageRenderer renderer, int height, int width, boolean addSelfReference) { - return makeTextCaptcha(saltSource, cipherHandler, password, renderer, height, width, captchaText, addSelfReference); - } - - /** - * Generates the completed captcha object with a picture based on the specified - * text, heigth and width. The implementation of creating the image is given by - * the specified renderer. The salt source specifies an arbitrary object used to - * salt the token. - * - * @param saltSource arbitrary object to be used to salt the solution hash for - * added security and to allow for authenticating the given - * answer - * @param password the password used to encrypt the implementation reference - * @param renderer Implementation of the ImageRenderer interface controlling - * how the specified captcha is generated as an image. - * @param height pixel height of the captcha image - * @param width pixel width of the catpcha image - * @param captchaText text the catpcha should display - * @return {@link TextCaptcha} containing the finalized captcha - */ - private TextCaptcha makeTextCaptcha(Serializable saltSource, CipherHandler cipherHandler, String password, TextImageRenderer renderer, - int height, int width, String captchaText, boolean addSelfReference) { - BufferedImage image = renderer.render(captchaText, height, width); - TextCaptcha captcha = null; - try { - byte[] imgData = convertImageToByteArray(image, IMG_FORMAT); - CompletableFuture selfreference = CompletableFuture.completedFuture(StringUtils.EMPTY); - if (addSelfReference) { - selfreference = CompletableFuture.supplyAsync(() -> addSelfReference(cipherHandler, saltSource, password)); - } - CompletableFuture token = CompletableFuture.supplyAsync(() -> makeToken(captchaText, saltSource)); - captcha = new TextCaptcha(imgData, token.get() + selfreference.get()); - } catch (InterruptedException e) { - log.fatal("Thread interruption during captcha generation: " + e.getLocalizedMessage()); - - } catch (ExecutionException e) { - log.fatal("Fatal error during captcha generation: " + e.getLocalizedMessage()); - } - return captcha; - } - -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/TextImageRenderer.java b/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/TextImageRenderer.java deleted file mode 100644 index 0dd3258..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/TextImageRenderer.java +++ /dev/null @@ -1,39 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.rendering; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.util.concurrent.ThreadLocalRandom; - -/** - * Describes the logic required to render - * - * @author Yannick Forster - */ -public interface TextImageRenderer { - - /** - * Creates a {@link BufferedImage} based on the input string, the pixel height - * and pixel width - * - * @param captchaTextInput String the image will show - * @param height the height of the image to be generated - * @param width the width of the image to be generated - * @return the generated image as {@link BufferedImage} - */ - BufferedImage render(final String captchaTextInput, int height, int width); - - /** - * Picks a random color from the array of possible text colors - * - * @param colors Color array from which to pick a random element. - * @return Color object, picked randomly out of the textCols-Field array - */ - default Color pickRandomColor(Color[] colors) { - if (colors.length == 1) { - return colors[0]; - } - ThreadLocalRandom r = ThreadLocalRandom.current(); - int i = r.nextInt(colors.length); - return colors[i]; - } -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/BackgroundPictureTextImageRenderer.java b/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/BackgroundPictureTextImageRenderer.java deleted file mode 100644 index 7c596ef..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/BackgroundPictureTextImageRenderer.java +++ /dev/null @@ -1,36 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.rendering.impl; - -import io.github.yaforster.flexcaptcha.textbased.rendering.TextImageRenderer; -import lombok.NonNull; -import lombok.RequiredArgsConstructor; -import org.apache.commons.lang3.StringUtils; - -import java.awt.*; -import java.awt.image.BufferedImage; - -/** - * @author Yannick Forster - */ -@RequiredArgsConstructor -public class BackgroundPictureTextImageRenderer extends SimpleTextImageRenderer implements TextImageRenderer { - - @NonNull - private BufferedImage backgroundimg; - - @Override - public final BufferedImage render(final String captchaTextInput, int height, int width) { - if (StringUtils.isEmpty(captchaTextInput)) { - throw new IllegalArgumentException("The specified captcha string is empty."); - } - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - Graphics2D graphic = image.createGraphics(); - graphic.setColor(getBackgrndCol()); - graphic.drawImage(backgroundimg, null, 0, 0); - drawDistortions(height, width, graphic); - drawText(captchaTextInput, image); - graphic.dispose(); - return image; - } - - -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/CleanTextImageRenderer.java b/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/CleanTextImageRenderer.java deleted file mode 100644 index 2b8ab53..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/CleanTextImageRenderer.java +++ /dev/null @@ -1,120 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.rendering.impl; - -import io.github.yaforster.flexcaptcha.textbased.rendering.TextImageRenderer; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.experimental.Accessors; -import org.apache.commons.lang3.StringUtils; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.util.stream.IntStream; - -/** - * Example implementation. - *

- * Renders a text captcha image without visual distortions in the finished - * image. - * - * @author Yannick Forster - */ -@SuppressWarnings("DuplicatedCode") -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -@Accessors(chain = true) -public class CleanTextImageRenderer implements TextImageRenderer { - - /** - * Color of the Captcha Background - */ - private Color backgrndCol = Color.white; - /** - * Set of possible colors of the letters in the captcha image - */ - private Color[] textCols = new Color[]{Color.blue}; - - /** - * The name of the font used to draw the letters - */ - private String fontName = "Verdana"; - - /** - * Renders a captcha image of specified height and widght of the given string - */ - @Override - public BufferedImage render(final String captchaTextInput, int height, int width) { - if (StringUtils.isEmpty(captchaTextInput)) { - throw new IllegalArgumentException("The specified captcha string is empty."); - } - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - Graphics2D graphic = image.createGraphics(); - graphic.setColor(backgrndCol); - graphic.fillRect(0, 0, width, height); - drawText(captchaTextInput, image); - graphic.dispose(); - return image; - } - - /** - * prepares the writing of the given captcha text onto the specified Graphics2d - * object - * - * @param captchaTextInput string containing the text to write - * @param image the image on which to draw - */ - private void drawText(String captchaTextInput, BufferedImage image) { - Graphics2D graphic = image.createGraphics(); - Font textFont = new Font(fontName, Font.BOLD, (int) (image.getHeight() / 2.5)); - graphic.setColor(pickRandomColor(textCols)); - graphic.setFont(textFont); - FontMetrics fontMetrics = graphic.getFontMetrics(); - int margin = image.getWidth() / 16; - int chars = captchaTextInput.length(); - float spaceForLetters = (-margin << 1) + image.getWidth(); - float spacePerChar = spaceForLetters / (chars - 1.0f); - IntStream.range(0, chars).boxed().forEachOrdered(i -> { - char charToDraw = captchaTextInput.charAt(i); - drawCharacter(image, textFont, fontMetrics, margin, spacePerChar, i, charToDraw); - }); - } - - /** - * Measures the font and draws each character of the given string to the - * Graphics2D object at a randomized angle. - * - * @param image the Graphics2D object containing the graphic in which the - * image is constructed - * @param textFont Font object containing the font in which the characters - * are to be drawn - * @param fontMetrics fontmetrics object used to measure the characters in the - * string - * @param margin calculated based on the width to define an approximate - * margin between each letter - * @param spacePerChar the space that the entire string will approximately - * require - * @param index running index of the character in the source string - * @param charToDraw the character to draw - */ - private void drawCharacter(BufferedImage image, Font textFont, FontMetrics fontMetrics, int margin, - float spacePerChar, Integer index, char charToDraw) { - int maxAdvance = fontMetrics.getMaxAdvance(); - int fontHeight = fontMetrics.getHeight(); - int charWidth = fontMetrics.charWidth(charToDraw); - int charDim = Math.max(maxAdvance, fontHeight); - BufferedImage charImage = new BufferedImage(charDim, charDim, BufferedImage.TYPE_INT_ARGB); - Graphics2D charGraphics = charImage.createGraphics(); - charGraphics.setColor(pickRandomColor(textCols)); - charGraphics.setFont(textFont); - int charX = (int) (0.5 * charDim - 0.5 * charWidth); - charGraphics.drawString(String.valueOf(charToDraw), charX, - (charDim - fontMetrics.getAscent()) / 2 + fontMetrics.getAscent()); - float x = margin + spacePerChar * (index.floatValue()) - charDim / 2.0f; - int y = (image.getHeight() - charDim) / 2; - image.createGraphics().drawImage(charImage, (int) x, y, charDim, charDim, null, null); - charGraphics.dispose(); - } -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/EffectChainTextImageRenderer.java b/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/EffectChainTextImageRenderer.java deleted file mode 100644 index 4f8e909..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/EffectChainTextImageRenderer.java +++ /dev/null @@ -1,50 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.rendering.impl; - -import com.jhlabs.image.AbstractBufferedImageOp; -import io.github.yaforster.flexcaptcha.textbased.rendering.TextImageRenderer; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.experimental.Accessors; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.util.ArrayList; -import java.util.List; - -/** - * Text Captcha rendering using a chain of Filters that will be applied to the generated image. - * - * @author Yannick Forster - */ -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -@Accessors(chain = true) -public class EffectChainTextImageRenderer implements TextImageRenderer { - - /** - * Set of possible colors of the letters in the captcha image - */ - private Color[] textCols = new Color[]{Color.blue, Color.red, Color.darkGray, Color.magenta, Color.black}; - - /** - * List of operations that will be applied to the image during rendering. - */ - private List bufferedOps = new ArrayList<>(0); - - @Override - public BufferedImage render(final String captchaTextInput, int height, int width) { - BufferedImage image = new SimpleTextImageRenderer().render(captchaTextInput, height, width); - if (!bufferedOps.isEmpty()) { - applyFilters(image); - } - return image; - } - - private void applyFilters(BufferedImage image) { - bufferedOps.forEach(op -> op.filter(image, image)); - } -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/SimpleTextImageRenderer.java b/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/SimpleTextImageRenderer.java deleted file mode 100644 index f60f983..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/SimpleTextImageRenderer.java +++ /dev/null @@ -1,165 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.rendering.impl; - -import io.github.yaforster.flexcaptcha.textbased.rendering.TextImageRenderer; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.experimental.Accessors; -import org.apache.commons.lang3.StringUtils; - -import java.awt.*; -import java.awt.geom.AffineTransform; -import java.awt.image.BufferedImage; -import java.security.SecureRandom; -import java.util.concurrent.ThreadLocalRandom; -import java.util.stream.IntStream; - -/** - * Example implementation. - *

- * Implements a rendering logic taking in an input string to generate a - * visualization of said string and return it in a base64 string representation - * for easier transportation - * - * @author Yannick Forster - */ -@SuppressWarnings("DuplicatedCode") -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -@Accessors(chain = true) -public class SimpleTextImageRenderer implements TextImageRenderer { - - /** - * Color of the Captcha Background - */ - private Color backgrndCol = Color.white; - /** - * Set of possible colors of the letters in the captcha image - */ - private Color[] textCols = new Color[]{Color.blue}; - /** - * Color of distortions in the image - */ - private Color distortCol = Color.white; - /** - * Defines the maximum angle that can be used to rotate a single character in - * the captcha - */ - private double maxrotateAngle = 0.45; - /** - * String name of the font to be used when writing characters to the image - */ - private String fontName = "Verdana"; - - /** - * Renders a captcha image of specified height and widght of the given string - */ - @Override - public BufferedImage render(final String captchaTextInput, int height, int width) { - if (StringUtils.isEmpty(captchaTextInput)) { - throw new IllegalArgumentException("The specified captcha string is empty."); - } - BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB); - Graphics2D graphic = image.createGraphics(); - graphic.setColor(backgrndCol); - graphic.fillRect(0, 0, width, height); - drawDistortions(height, width, graphic); - drawText(captchaTextInput, image); - graphic.dispose(); - return image; - } - - /** - * Draws distortions onto the given Graphics2D object in the shape of randomly - * generated rectangles and dots to help obscure the text in the captcha image - * - * @param height pixel height of the image inside the graphics object - * @param width pixel width of the image inside the graphics object - * @param graphic the Graphics2D object onto which the distortions are to be - * drawn - */ - protected void drawDistortions(int height, int width, Graphics2D graphic) { - graphic.setColor(distortCol); - SecureRandom rnd = new SecureRandom(); - for (int i = 0; i < width / 64; i++) { - int L = (int) (rnd.nextDouble() * height / 2.0); - int X = (int) (rnd.nextDouble() * width - L); - int Y = (int) (rnd.nextDouble() * height - L); - graphic.draw3DRect(X, Y, L << 1, L << 1, true); - } - Color darkerBackgrnd = backgrndCol.darker(); - graphic.setColor(darkerBackgrnd); - ThreadLocalRandom random = ThreadLocalRandom.current(); - int dotCount = height * width / 4; - for (int j = 0; j < dotCount; j++) { - int x = Math.abs(random.nextInt()) % width; - int y = Math.abs(random.nextInt()) % height; - graphic.drawLine(x, y, x, y); - } - } - - /** - * prepares the writing of the given captcha text onto the specified Graphics2d - * object - * - * @param captchaTextInput string containing the text to write - * @param image the image on which to draw the text - */ - protected void drawText(String captchaTextInput, BufferedImage image) { - Graphics2D graphic = image.createGraphics(); - Font textFont = new Font(fontName, Font.BOLD, (int) (image.getHeight() / 2.5)); - graphic.setColor(pickRandomColor(textCols)); - graphic.setFont(textFont); - FontMetrics fontMetrics = graphic.getFontMetrics(); - int margin = image.getWidth() / 16; - int chars = captchaTextInput.length(); - float spaceForLetters = (-margin << 1) + image.getWidth(); - float spacePerChar = spaceForLetters / (chars - 1.0f); - IntStream.range(0, chars).boxed().forEachOrdered(i -> { - char charToDraw = captchaTextInput.charAt(i); - drawCharacter(image, textFont, fontMetrics, margin, spacePerChar, i, charToDraw); - }); - } - - /** - * Measures the font and draws each character of the given string to the - * Graphics2D object at a randomized angle. - * - * @param textFont Font object containing the font in which the characters - * are to be drawn - * @param fontMetrics fontmetrics object used to measure the characters in the - * string - * @param margin calculated based on the width to define an approximate - * margin between each letter - * @param spacePerChar the space that the entire string will approximately - * require - * @param index running index of the character in the source string - */ - private void drawCharacter(BufferedImage image, Font textFont, FontMetrics fontMetrics, int margin, - float spacePerChar, Integer index, char charToDraw) { - int maxAdvance = fontMetrics.getMaxAdvance(); - int fontHeight = fontMetrics.getHeight(); - int charWidth = fontMetrics.charWidth(charToDraw); - int charDim = Math.max(maxAdvance, fontHeight); - int halfCharDim = charDim / 2; - BufferedImage charImage = new BufferedImage(charDim, charDim, BufferedImage.TYPE_INT_ARGB); - Graphics2D charGraphics = charImage.createGraphics(); - charGraphics.translate(halfCharDim, halfCharDim); - double angle = (new SecureRandom().nextDouble() - 0.5) * maxrotateAngle; - charGraphics.transform(AffineTransform.getRotateInstance(angle)); - charGraphics.translate(-halfCharDim, -halfCharDim); - charGraphics.setColor(pickRandomColor(textCols)); - charGraphics.setFont(textFont); - int charX = (int) (0.5 * charDim - 0.5 * charWidth); - charGraphics.drawString(String.valueOf(charToDraw), charX, - (charDim - fontMetrics.getAscent()) / 2 + fontMetrics.getAscent()); - float x = margin + spacePerChar * (index.floatValue()) - charDim / 2.0f; - int y = (image.getHeight() - charDim) / 2; - image.createGraphics().drawImage(charImage, (int) x, y, charDim, charDim, null, null); - charGraphics.dispose(); - } - -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/TwirledTextImageRenderer.java b/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/TwirledTextImageRenderer.java deleted file mode 100644 index 786c9d9..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/TwirledTextImageRenderer.java +++ /dev/null @@ -1,61 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.rendering.impl; - -import com.jhlabs.image.TwirlFilter; -import io.github.yaforster.flexcaptcha.textbased.rendering.TextImageRenderer; -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import lombok.experimental.Accessors; - -import java.awt.*; -import java.awt.image.BufferedImage; -import java.util.concurrent.ThreadLocalRandom; - -/** - * Text image handler that adds a twirl effect to the rendered text captcha - * image - * - * @author Yannick Forster - */ -@Getter -@Setter -@AllArgsConstructor -@NoArgsConstructor -@Accessors(chain = true) -public class TwirledTextImageRenderer implements TextImageRenderer { - - /** - * Set of possible colors of the letters in the captcha image - */ - private Color[] textCols = new Color[]{Color.blue, Color.red, Color.darkGray, Color.magenta, Color.black}; - /** - * Radius integer used for the twirling effect. Higher numbers result in a - * stronger effect. 10 per default. - */ - private float twirlStrength = -0.3f; - - @Override - public BufferedImage render(final String captchaTextInput, int height, int width) { - SimpleTextImageRenderer simpleRenderer = new SimpleTextImageRenderer(); - simpleRenderer.setTextCols(textCols); - BufferedImage image = simpleRenderer.render(captchaTextInput, height, width); - return applytwirl(image); - } - - /** - * Applies a twisting effect of the entire graphic originating in the center of - * the image - */ - private BufferedImage applytwirl(BufferedImage image) { - TwirlFilter filter = new TwirlFilter(); - ThreadLocalRandom random = ThreadLocalRandom.current(); - float angle = twirlStrength; - if (random.nextBoolean()) { - angle = angle * (-1); - } - filter.setAngle(angle); - return filter.filter(image, new BufferedImage(image.getWidth(), image.getHeight(), image.getType())); - } - -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/textbased/textgen/CaptchaTextGenerator.java b/src/main/java/io/github/yaforster/flexcaptcha/textbased/textgen/CaptchaTextGenerator.java deleted file mode 100644 index 7c93b57..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/textbased/textgen/CaptchaTextGenerator.java +++ /dev/null @@ -1,72 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.textgen; - -import io.github.yaforster.flexcaptcha.textbased.enums.Case; - -/** - * Interface for declaring methods used for to generate randomized Strings - * - * @author Yannick Forster - */ -public interface CaptchaTextGenerator { - - /** - * String containing every character that is allowed for the generation of - * Strings. Excludes some characters by default that may be confusing when - * rotated in a certain way. - */ - String DEFAULT_CHARACTER_BASE = "abcdefghjkmpqrstuvwxy2345689"; - - /** - * Generates a new randomized String of mixed case letters and numbers of the - * given length - * - * @param length the generated string is supposed to have - * @return randomized String of mixed case letters and numbers of the given - * length - */ - default String generate(int length) { - return generate(length, DEFAULT_CHARACTER_BASE, Case.MIXEDCASE); - } - - /** - * Generates a new randomized String of the specified length consisting of a - * randomly selected set of characters from the given string - * - * @param length the generated string is supposed to have - * @param characterbase String consisting of the set of letters from which the - * method will randomly pick characters - * @return randomized String of the specified length consisting of a randomly - * selected set of characters from the given string - */ - default String generate(int length, String characterbase) { - return generate(length, characterbase, Case.MIXEDCASE); - } - - /** - * Generates a new randomized String of letters and numbers of the given length - * and specified case - * - * @param length the generated string is supposed to have - * @param charCase Case enum with either lower-, upper-, or mixed case. - * @return randomized String of letters and numbers of the given length and - * specified case - */ - default String generate(int length, Case charCase) { - return generate(length, DEFAULT_CHARACTER_BASE, charCase); - } - - /** - * Generates a new randomized String of the specified length consisting of a - * randomly selected set of characters from the given string, generated as - * either upper-, lower- or mixed case, depending on the case specified - * - * @param length the generated string is supposed to have - * @param characterbase String consisting of the set of letters from which the - * method will randomly pick characters * @param charCase - * @param charCase Case enum with either lower-, upper-, or mixed case. - * @return randomized String of the specified length consisting of a randomly - * selected set of characters from the given string - */ - String generate(int length, String characterbase, Case charCase); - -} \ No newline at end of file diff --git a/src/main/java/io/github/yaforster/flexcaptcha/textbased/textgen/impl/SimpleCaptchaTextGenerator.java b/src/main/java/io/github/yaforster/flexcaptcha/textbased/textgen/impl/SimpleCaptchaTextGenerator.java deleted file mode 100644 index c4b2d28..0000000 --- a/src/main/java/io/github/yaforster/flexcaptcha/textbased/textgen/impl/SimpleCaptchaTextGenerator.java +++ /dev/null @@ -1,107 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.textgen.impl; - -import io.github.yaforster.flexcaptcha.textbased.enums.Case; -import io.github.yaforster.flexcaptcha.textbased.textgen.CaptchaTextGenerator; -import lombok.Getter; -import lombok.Setter; -import org.apache.commons.lang3.StringUtils; - -import java.util.concurrent.ThreadLocalRandom; -import java.util.stream.IntStream; - -/** - * @author Yannick Forster - *

- * This class is used to provide logic to generate or accept a String - * representing the image content and the solution for the Captcha for - * further processing - */ -@Getter -@Setter -public class SimpleCaptchaTextGenerator implements CaptchaTextGenerator { - - /** - * Checks the given Case enum and modifies the case of the character based on - * the enum. - * - * @param charCase Case enum defining the character to either be lowercase, - * uppercase or random case. - * @param c the character - * @return modified char - */ - private static char setCase(Case charCase, char c) { - switch (charCase) { - case LOWERCASE: - c = Character.toLowerCase(c); - break; - case UPPERCASE: - c = Character.toUpperCase(c); - break; - default: - ThreadLocalRandom random = ThreadLocalRandom.current(); - if (random.nextBoolean()) { - c = Character.toUpperCase(c); - } - break; - } - return c; - } - - /** - * Returns a single character from the String at a random position in the string - * - * @param src source String from which the character is randomly pulled - * @return character pulled from the String at random point - */ - private static char pickRandomChar(String src) { - ThreadLocalRandom random = ThreadLocalRandom.current(); - int index = random.nextInt(src.length()); - return src.charAt(index); - } - - /** - * Constructs the output String by repeatedly copying a single character from - * the characterbase-String at a random point until the specified length is - * reached. The case-enum controls whether the case of the letters. Mixed - * case will randomize the case of each character every time it is picked from - * the source string - * - * @param length the generated string is supposed to have - * @param characterbase String consisting of the set of letters from which the - * method will randomly pick characters - * @param charCase Case enum with either lower-, upper-, or mixed case. - * @return randomized String of the specified length consisting of a randomly - * selected set of characters from the given string - */ - private static String getRandomLetters(int length, String characterbase, Case charCase) { - StringBuffer charbuf = new StringBuffer(0); - IntStream.range(0, length).forEach(i -> { - char c = pickRandomChar(characterbase); - c = setCase(charCase, c); - charbuf.append(c); - }); - return charbuf.toString(); - } - - /** - * Generates a new randomized String of the specified length consisting of a - * randomly selected set of characters from the given string, generated as - * either upper-, lower- or mixed case, depending on the case specified - * - * @param length the generated string is supposed to have - * @param characterbase String consisting of the set of letters from which the - * method will randomly pick characters - * @param charCase Case enum with either lower-, upper-, or mixed case. - * @return randomized String of the specified length consisting of a randomly - * selected set of characters from the given string - */ - @Override - public String generate(int length, String characterbase, Case charCase) { - if (StringUtils.isEmpty(characterbase)) { - throw new IllegalArgumentException( - "the specified character base from which to draw the captcha characters is empty."); - } - return getRandomLetters(length, characterbase, charCase); - } - -} diff --git a/src/main/java/io/github/yaforster/flexcaptcha/util/AbstractTextGenerator.java b/src/main/java/io/github/yaforster/flexcaptcha/util/AbstractTextGenerator.java new file mode 100644 index 0000000..131017d --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/util/AbstractTextGenerator.java @@ -0,0 +1,61 @@ +package io.github.yaforster.flexcaptcha.util; + +import org.apache.commons.lang3.StringUtils; + +/** + * Interface for declaring methods used for to generate randomized Strings + * + * @author Yannick Forster + */ +public abstract class AbstractTextGenerator { + + /** + * String containing every character that is allowed for the generation of + * Strings. Excludes some characters by default that may be confusing when + * rotated in a certain way. + */ + protected final static String DEFAULT_CHARACTER_BASE = "abcdefghjkmpqrstuvwxy2345689"; + /** + * String consisting of the set of letters from which the method will randomly pick characters + */ + protected final String characterbase; + + protected AbstractTextGenerator(String characterbase) { + this.characterbase = getCharCaseOrDefault(characterbase); + } + + protected static String getCharCaseOrDefault(String characterbase) { + if (characterbase != null && !StringUtils.isBlank(characterbase)) { + return characterbase; + } + else { + return DEFAULT_CHARACTER_BASE; + } + } + + + /** + * Generates a new randomized String of the specified length consisting of a + * randomly selected set of characters from the given string + * + * @param length the generated string is supposed to have + * @return randomized String of the specified length consisting of a randomly + * selected set of characters from the given string + */ + public final String generate(int length) { + return generate(length, TextCase.MIXEDCASE); + } + + /** + * Generates a new randomized String of the specified length consisting of a + * randomly selected set of characters from the given string, generated as + * either upper-, lower- or mixed case, depending on the case specified + * + * @param length the generated string is supposed to have + * @param charTextCase Case enum with either lower-, upper-, or mixed case. + * @return randomized String of the specified length consisting of a randomly + * selected set of characters from the given string + */ + abstract String generate(int length, TextCase charTextCase); + +} \ No newline at end of file diff --git a/src/main/java/io/github/yaforster/flexcaptcha/textbased/enums/Case.java b/src/main/java/io/github/yaforster/flexcaptcha/util/TextCase.java similarity index 50% rename from src/main/java/io/github/yaforster/flexcaptcha/textbased/enums/Case.java rename to src/main/java/io/github/yaforster/flexcaptcha/util/TextCase.java index f7bacc3..401527c 100644 --- a/src/main/java/io/github/yaforster/flexcaptcha/textbased/enums/Case.java +++ b/src/main/java/io/github/yaforster/flexcaptcha/util/TextCase.java @@ -1,4 +1,4 @@ -package io.github.yaforster.flexcaptcha.textbased.enums; +package io.github.yaforster.flexcaptcha.util; import lombok.Getter; @@ -8,28 +8,19 @@ * @author Yannick Forster */ @Getter -public enum Case { +public enum TextCase { /** * Uppercase letters only */ - UPPERCASE(0), + UPPERCASE, /** * Lowercase letters only */ - LOWERCASE(1), + LOWERCASE, /** * Both uppercase and lowercase letters */ - MIXEDCASE(2); + MIXEDCASE - /** - * A number representing the available cases - */ - private final int caseNum; - - Case(int caseNum) { - this.caseNum = caseNum; - } - -} +} \ No newline at end of file diff --git a/src/main/java/io/github/yaforster/flexcaptcha/util/TextGenerator.java b/src/main/java/io/github/yaforster/flexcaptcha/util/TextGenerator.java new file mode 100644 index 0000000..27c93fd --- /dev/null +++ b/src/main/java/io/github/yaforster/flexcaptcha/util/TextGenerator.java @@ -0,0 +1,105 @@ +package io.github.yaforster.flexcaptcha.util; + +import lombok.Builder; + +import java.security.SecureRandom; +import java.util.stream.IntStream; + +/** + * @author Yannick Forster + *

+ * This class is used to provide logic to generate or accept a String + * representing the image content and the solution for the Captcha for + * further processing + */ +public class TextGenerator extends AbstractTextGenerator { + + @Builder + public TextGenerator(String characterbase) { + super(characterbase); + } + + /** + * Checks the given Case enum and modifies the case of the character based on + * the enum. + * + * @param charTextCase Case enum defining the character to either be lowercase, + * uppercase or random case. + * @param c the character + * @return modified char + */ + private static char setCase(TextCase charTextCase, char c) { + return switch (charTextCase) { + case LOWERCASE: + yield Character.toLowerCase(c); + case UPPERCASE: + yield Character.toUpperCase(c); + default: + SecureRandom random = new SecureRandom(); + if (random.nextBoolean()) { + yield Character.toUpperCase(c); + } + else { + yield c; + } + }; + } + + /** + * Returns a single character from the String at a random position in the string + * + * @param src source String from which the character is randomly pulled + * @return character pulled from the String at random point + */ + private static char pickRandomChar(String src) { + SecureRandom random = new SecureRandom(); + int index = random.nextInt(src.length()); + return src.charAt(index); + } + + /** + * Constructs the output String by repeatedly copying a single character from + * the characterbase-String at a random point until the specified length is + * reached. The case-enum controls whether the case of the letters. Mixed + * case will randomize the case of each character every time it is picked from + * the source string + * + * @param length the generated string is supposed to have + * @param charTextCase Case enum with either lower-, upper-, or mixed case. + * @return randomized String of the specified length consisting of a randomly + * selected set of characters from the given string + */ + private String getRandomLetters(int length, TextCase charTextCase) { + StringBuffer charbuf = new StringBuffer(0); + IntStream.range(0, length).forEach(i -> { + appendBufferByRandomCharOfCase(charTextCase, charbuf); + }); + return charbuf.toString(); + } + + private void appendBufferByRandomCharOfCase(TextCase charTextCase, StringBuffer charbuf) { + char c = pickRandomCharWithCase(charTextCase); + charbuf.append(c); + } + + private char pickRandomCharWithCase(TextCase charTextCase) { + char randomChar = pickRandomChar(characterbase); + return setCase(charTextCase, randomChar); + } + + /** + * Generates a new randomized String of the specified length consisting of a + * randomly selected set of characters from the given string, generated as + * either upper-, lower- or mixed case, depending on the case specified + * + * @param length the generated string is supposed to have + * @param charTextCase Case enum with either lower-, upper-, or mixed case. + * @return randomized String of the specified length consisting of a randomly + * selected set of characters from the given string + */ + @Override + public final String generate(int length, TextCase charTextCase) { + return getRandomLetters(length, charTextCase); + } + +} \ No newline at end of file diff --git a/src/main/resources/log4j2.xml b/src/main/resources/log4j2.xml index cffe811..eb50e2e 100644 --- a/src/main/resources/log4j2.xml +++ b/src/main/resources/log4j2.xml @@ -4,7 +4,7 @@ - + diff --git a/src/test/java/io/github/yaforster/flexcaptcha/CaptchaGeneratorTest.java b/src/test/java/io/github/yaforster/flexcaptcha/CaptchaGeneratorTest.java new file mode 100644 index 0000000..3402de4 --- /dev/null +++ b/src/test/java/io/github/yaforster/flexcaptcha/CaptchaGeneratorTest.java @@ -0,0 +1,40 @@ +package io.github.yaforster.flexcaptcha; + +import io.github.yaforster.flexcaptcha.core.Captcha; +import io.github.yaforster.flexcaptcha.core.CaptchaGenerator; +import io.github.yaforster.flexcaptcha.impl.rendering.CaptchaRenderer; +import io.github.yaforster.flexcaptcha.impl.token.CaptchaCipher; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class CaptchaGeneratorTest { + + @Test + void generate_with_default_should_work() { + CaptchaCipher cipher = CaptchaCipher.builder().build(); + CaptchaRenderer renderer = CaptchaRenderer.getDefaultCaptchaRenderer(); + CaptchaGenerator generator = new CaptchaGenerator(cipher, renderer); + assertDoesNotThrow(() -> generator.generate("abc123", "salt")); + } + + @Test + void test_validation_should_work() { + CaptchaCipher cipher = CaptchaCipher.builder().build(); + CaptchaRenderer renderer = CaptchaRenderer.getDefaultCaptchaRenderer(); + CaptchaGenerator generator = new CaptchaGenerator(cipher, renderer); + assertTrue(generator.validate("vnBn8x3bpm3wkvJYANdy9VNRijRowlFyq72US0ja4Jo=", "aBc123", "salt")); + } + + @Test + void test_can_validate_own_tokens() { + CaptchaCipher cipher = CaptchaCipher.builder().build(); + CaptchaRenderer renderer = CaptchaRenderer.getDefaultCaptchaRenderer(); + CaptchaGenerator generator = new CaptchaGenerator(cipher, renderer); + String solution = "aBc123"; + String someSalt = "salt"; + Captcha captcha = generator.generate(solution, someSalt); + assertTrue(generator.validate(captcha.token(), solution, someSalt)); + } +} \ No newline at end of file diff --git a/src/test/java/io/github/yaforster/flexcaptcha/CaptchaHandlerTest.java b/src/test/java/io/github/yaforster/flexcaptcha/CaptchaHandlerTest.java deleted file mode 100644 index a8f5253..0000000 --- a/src/test/java/io/github/yaforster/flexcaptcha/CaptchaHandlerTest.java +++ /dev/null @@ -1,76 +0,0 @@ -package io.github.yaforster.flexcaptcha; - -import org.apache.commons.lang3.NotImplementedException; -import org.junit.Test; -import org.mockito.Mockito; - -import javax.crypto.spec.IvParameterSpec; -import java.awt.*; -import java.io.ByteArrayOutputStream; -import java.io.IOException; -import java.io.ObjectOutputStream; - -import static org.junit.Assert.assertArrayEquals; -import static org.junit.Assert.assertEquals; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; - -/** - * Tests default methods of the {@link CaptchaHandler} interface using an - * arbitrary implementation - *

- * - * @author Yannick Forster - */ -public class CaptchaHandlerTest { - - private final CipherHandler cipherHandler = getCHMock(); - private final String password = "ThisIsMyPassword"; - private final Button dummyObj = new Button(); - private final CaptchaHandler captchaHandler = makeCaptchaHandler(); - - @Test - public void testAddSelfReference() { - String selfReference = captchaHandler.addSelfReference(cipherHandler, dummyObj, password); - String selfReferenceBase64 = selfReference.split(CaptchaHandler.DELIMITER)[1]; - assertEquals("AQID", selfReferenceBase64); - } - - @Test - public void testGetSaltObjectBytes() { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); - ObjectOutputStream oos = new ObjectOutputStream(baos)) { - oos.writeObject(dummyObj); - baos.close(); - oos.close(); - byte[] testbytes = baos.toByteArray(); - byte[] methodTestBytes = captchaHandler.getSaltObjectBytes(dummyObj); - assertArrayEquals(testbytes, methodTestBytes); - } catch (IOException e) { - e.printStackTrace(); - } - } - - /* - * === Test setup === - */ - - private CipherHandler getCHMock() { - CipherHandler cipherHandler = Mockito.mock(CipherHandler.class); - Mockito.when(cipherHandler.generateIV()) - .thenReturn(new IvParameterSpec(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})); - Mockito.when(cipherHandler.decryptString(any(byte[].class), anyString(), any())) - .thenReturn(new byte[]{1, 2, 3}); - Mockito.when(cipherHandler.encryptString(any(byte[].class), anyString(), any(), any(byte[].class))) - .thenReturn(new byte[]{1, 2, 3}); - return cipherHandler; - - } - - private CaptchaHandler makeCaptchaHandler() { - return (answer, token, cipherHandler, saltSource, password) -> { - throw new NotImplementedException(); - }; - } - -} diff --git a/src/test/java/io/github/yaforster/flexcaptcha/CaptchaTest.java b/src/test/java/io/github/yaforster/flexcaptcha/CaptchaTest.java new file mode 100644 index 0000000..59ae30d --- /dev/null +++ b/src/test/java/io/github/yaforster/flexcaptcha/CaptchaTest.java @@ -0,0 +1,16 @@ +package io.github.yaforster.flexcaptcha; + +import io.github.yaforster.flexcaptcha.core.Captcha; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class CaptchaTest { + + @Test + void getImgDataAsBase64() { + Captcha captcha = new Captcha("someToken", new byte[]{1, 2, 3, 4, 5}); + String imgBytesBase64 = captcha.getImgDataAsBase64(); + assertEquals("AQIDBAU=", imgBytesBase64); + } +} \ No newline at end of file diff --git a/src/test/java/io/github/yaforster/flexcaptcha/CipherHandlerTest.java b/src/test/java/io/github/yaforster/flexcaptcha/CipherHandlerTest.java deleted file mode 100644 index 9072377..0000000 --- a/src/test/java/io/github/yaforster/flexcaptcha/CipherHandlerTest.java +++ /dev/null @@ -1,97 +0,0 @@ -package io.github.yaforster.flexcaptcha; - -import org.junit.Test; - -import java.awt.*; - -import static org.junit.Assert.*; - -public class CipherHandlerTest { - - private final CipherHandler ch = new CipherHandler(); - private final byte[] inputBytes = "TestString".getBytes(); - private final String password = "ThisIsMyPassword"; - private final Button dummyObj = new Button(); - private final byte[] ivBytes = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16}; - - private final byte[] encrExpected = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, -32, 13, -33, -117, 18, - 92, 6, 17, -66, -63, -118, 122, -18, 119, -57, -13}; - private final byte[] decrExpected = new byte[]{84, 101, 115, 116, 83, 116, 114, 105, 110, 103}; - private final byte[] encrNoSaltSourceExpected = new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, -80, -47, -48, - 81, 125, 62, 101, 85, -9, 21, -103, -91, 94, 95, 80, 88}; - private final byte[] decrNoSaltSourceExpected = new byte[]{84, 101, 115, 116, 83, 116, 114, 105, 110, 103}; - - @Test - public void testEncryptString_ShouldWork() { - byte[] encrypted = ch.encryptString(inputBytes, password, dummyObj, ivBytes); - assertArrayEquals(encrypted, encrExpected); - } - - @Test - public void testEncryptString_NoInput_ShouldFail() { - assertThrows(IllegalArgumentException.class, () -> ch.encryptString(null, password, dummyObj, ivBytes)); - } - - @Test - public void testEncryptString_NoPassword_ShouldFail() { - assertThrows(IllegalArgumentException.class, () -> ch.encryptString(inputBytes, null, dummyObj, ivBytes)); - } - - @Test - public void testEncryptString_NoSaltSource_ShouldWork() { - byte[] encrypted = ch.encryptString(inputBytes, password, null, ivBytes); - assertEquals(32, encrypted.length); - } - - @Test - public void testEncryptString_EmptySaltSource_ShouldWork() { - byte[] encrypted = ch.encryptString(inputBytes, password, "", ivBytes); - assertEquals(32, encrypted.length); - } - - @Test - public void testEncryptString_NullIVBytes_ShouldFail() { - assertThrows(NullPointerException.class, () -> ch.encryptString(inputBytes, password, "", null)); - } - - @Test - public void testEncryptString_NoIVBytes_ShouldWork() { - byte[] encrypted = ch.encryptString(inputBytes, password, ""); - assertEquals(32, encrypted.length); - } - - @Test - public void testDecryptString_ShouldWork() { - byte[] decrypted = ch.decryptString(encrExpected, password, dummyObj); - assertArrayEquals(decrypted, decrExpected); - } - - @Test - public void testDecryptString_NoInput_ShouldFail() { - assertThrows(NullPointerException.class, () -> ch.decryptString(null, password, dummyObj)); - } - - @Test - public void testDecryptString_NoPassword_ShouldFail() { - assertThrows(IllegalArgumentException.class, () -> ch.decryptString(inputBytes, null, dummyObj)); - } - - @Test - public void testDecryptString_NoSaltSource_ShouldWork() { - byte[] decrypted = ch.decryptString(encrNoSaltSourceExpected, password, null); - assertArrayEquals(decrypted, decrNoSaltSourceExpected); - } - - @Test - public void testDecryptString_EmptySaltSource_ShouldFail() { - byte[] decrypted = ch.decryptString(encrNoSaltSourceExpected, password, ""); - assertNull(decrypted); - } - - @Test - public void testGenerateIV_ShouldWork() { - byte[] iv = new CipherHandler().generateIV().getIV(); - assertEquals(16, iv.length); - } - -} diff --git a/src/test/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaCipherTest.java b/src/test/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaCipherTest.java new file mode 100644 index 0000000..53dbc4f --- /dev/null +++ b/src/test/java/io/github/yaforster/flexcaptcha/core/AbstractCaptchaCipherTest.java @@ -0,0 +1,64 @@ +package io.github.yaforster.flexcaptcha.core; + +import io.github.yaforster.flexcaptcha.impl.token.CaptchaCipher; +import io.github.yaforster.flexcaptcha.impl.token.CipherInstantiationException; +import io.github.yaforster.flexcaptcha.impl.token.CipherSettings; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import javax.crypto.Cipher; +import javax.crypto.NoSuchPaddingException; +import java.security.NoSuchAlgorithmException; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; + +class AbstractCaptchaCipherTest { + + private static final String FICTIONAL_PASSWORD = "somepassword"; + private static final String FICTIONAL_SALT = "salt"; + private static final byte[] FICTIONAL_IV = {-71, 11, 45, -61, 44, -22, 5, -21, -91, -80, -121, -48, 29, -31, -25, + 76}; + + @Test + final void getCipher_shouldWork() { + CaptchaCipher captchaCipher = CaptchaCipher.builder().build(); + assertDoesNotThrow(() -> captchaCipher.getCipher(FICTIONAL_PASSWORD, FICTIONAL_SALT, Cipher.ENCRYPT_MODE, + FICTIONAL_IV)); + } + + @Test + final void getCipher_should_Throw_and_map_SuchPaddingException() { + try (MockedStatic cipherMockedStatic = Mockito.mockStatic(Cipher.class)) { + cipherMockedStatic.when(() -> Cipher.getInstance(any())).thenThrow(NoSuchPaddingException.class); + CaptchaCipher captchaCipher = CaptchaCipher.builder().build(); + CipherInstantiationException exception = assertThrows(CipherInstantiationException.class, + () -> captchaCipher.getCipher(FICTIONAL_PASSWORD, FICTIONAL_SALT, Cipher.ENCRYPT_MODE, + FICTIONAL_IV)); + assertTrue(exception.getLocalizedMessage().contains("Unknown padding specified for token encryption")); + } + } + + @Test + final void getCipher_should_Throw_and_map_NoSuchAlgorithmException() { + try (MockedStatic cipherMockedStatic = Mockito.mockStatic(Cipher.class)) { + cipherMockedStatic.when(() -> Cipher.getInstance(any())).thenThrow(NoSuchAlgorithmException.class); + CaptchaCipher captchaCipher = CaptchaCipher.builder().build(); + CipherInstantiationException exception = assertThrows(CipherInstantiationException.class, + () -> captchaCipher.getCipher(FICTIONAL_PASSWORD, FICTIONAL_SALT, Cipher.ENCRYPT_MODE, + FICTIONAL_IV)); + assertTrue(exception.getLocalizedMessage().contains("Unknown padding specified for token encryption")); + } + } + + @Test + final void getCipher_should_Throw_and_map_anything_else() { + CipherSettings invalidCipherSettings = new CipherSettings("PBKDF2WithHmacSHA256", "AES/CBC/PKCS5Padding", + "somethingBroken"); + CaptchaCipher captchaCipher = CaptchaCipher.builder().cipherSettings(invalidCipherSettings).build(); + CipherInstantiationException exception = assertThrows(CipherInstantiationException.class, + () -> captchaCipher.getCipher(FICTIONAL_PASSWORD, FICTIONAL_SALT, Cipher.ENCRYPT_MODE, FICTIONAL_IV)); + assertTrue(exception.getLocalizedMessage().contains("Fatal error during token encryption")); + } +} \ No newline at end of file diff --git a/src/test/java/io/github/yaforster/flexcaptcha/core/CaptchaTest.java b/src/test/java/io/github/yaforster/flexcaptcha/core/CaptchaTest.java deleted file mode 100644 index c2616b2..0000000 --- a/src/test/java/io/github/yaforster/flexcaptcha/core/CaptchaTest.java +++ /dev/null @@ -1,22 +0,0 @@ -package io.github.yaforster.flexcaptcha.core; - -import io.github.yaforster.flexcaptcha.Captcha; -import io.github.yaforster.flexcaptcha.textbased.TextCaptcha; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; - -/** - * Tests {@link Captcha} - * - * @author Yannick Forster - */ -public class CaptchaTest { - - @Test - public void testGetImgDataAsBase64() { - TextCaptcha captcha = new TextCaptcha(new byte[]{-119, 80, 78, 71, 13, 10, 26, 10, 0, 0, 0, 13, 73, 72}, "ABC"); - assertEquals("iVBORw0KGgoAAAANSUg=", captcha.getImgDataAsBase64()); - } - -} diff --git a/src/test/java/io/github/yaforster/flexcaptcha/imgbased/handling/ImageCaptchaHandlerTest.java b/src/test/java/io/github/yaforster/flexcaptcha/imgbased/handling/ImageCaptchaHandlerTest.java deleted file mode 100644 index 37f02f4..0000000 --- a/src/test/java/io/github/yaforster/flexcaptcha/imgbased/handling/ImageCaptchaHandlerTest.java +++ /dev/null @@ -1,165 +0,0 @@ -package io.github.yaforster.flexcaptcha.imgbased.handling; - -import io.github.yaforster.flexcaptcha.CipherHandler; -import io.github.yaforster.flexcaptcha.imgbased.ImageCaptcha; -import io.github.yaforster.flexcaptcha.imgbased.handling.impl.SimpleImageCaptchaHandler; -import org.apache.commons.lang3.ArrayUtils; -import org.junit.Test; -import org.mockito.Mockito; - -import javax.crypto.spec.IvParameterSpec; -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.Arrays; -import java.util.stream.Stream; - -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; - -/** - * Tests the default methods of the {@link ImageCaptchaHandler} interface using - * an arbitrary implementation - * - * @author Yannick Forster - */ -@SuppressWarnings("ZeroLengthArrayAllocation") -public class ImageCaptchaHandlerTest { - - private final ImageCaptchaHandler handler = new SimpleImageCaptchaHandler(); - private final BufferedImage[] dummyArr = new BufferedImage[]{new BufferedImage(10, 10, BufferedImage.TYPE_3BYTE_BGR)}; - private final BufferedImage[] dummyArr2 = new BufferedImage[]{new BufferedImage(15, 15, BufferedImage.TYPE_4BYTE_ABGR)}; - private final Button dummySerializable = new Button(); - private final String password = "ThisIsMyPassword!"; - private final CipherHandler cipherHandler = getCHMock(); - - @Test - public void testWithDummyObjs_ShouldWork() { - ImageCaptcha captcha = handler.generate(2, cipherHandler, dummySerializable, password, dummyArr, dummyArr2, - true); - assertTrue(captcha.getToken().length() > 0); - assertTrue(ArrayUtils.isNotEmpty(captcha.getImgData())); - } - - @Test - public void testWithDifferentDummyObjs_ShouldWork() { - ImageCaptcha captcha = handler.generate(2, cipherHandler, dummySerializable, password, dummyArr, dummyArr2, - true); - assertTrue(captcha.getToken().length() > 0); - assertTrue(Arrays.stream(captcha.getImgData()).distinct().count() > 2); - } - - @Test - public void testNullSerializable_Shouldwork() { - ImageCaptcha captcha = handler.generate(2, cipherHandler, null, password, dummyArr, dummyArr2, true); - assertTrue(captcha.getToken().length() > 0); - assertTrue(ArrayUtils.isNotEmpty(captcha.getImgData())); - } - - @Test - public void testEmptySerializable_Shouldwork() { - ImageCaptcha captcha = handler.generate(2, cipherHandler, "", password, dummyArr, dummyArr2, true); - assertTrue(captcha.getToken().length() > 0); - assertTrue(ArrayUtils.isNotEmpty(captcha.getImgData())); - } - - @Test - public void testNullArrays() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(2, cipherHandler, null, password, null, null, true)); - } - - @Test - public void testEmptyArrays() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(2, cipherHandler, null, password, new BufferedImage[]{}, new BufferedImage[]{}, true)); - } - - @Test - public void testNullSolutionArray() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(2, cipherHandler, null, password, dummyArr, null, true)); - } - - @Test - public void testEmptySolutionArray() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(2, cipherHandler, null, password, new BufferedImage[]{}, null, true)); - } - - @Test - public void testNullFillArray() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(2, cipherHandler, null, password, null, dummyArr, true)); - } - - @Test - public void testEmptyFillArray() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(2, cipherHandler, null, password, dummyArr, new BufferedImage[]{}, true)); - } - - @Test - public void autoResizeTest30x30() { - BufferedImage smallImage = new BufferedImage(10, 10, BufferedImage.TYPE_3BYTE_BGR); - BufferedImage largeImage = new BufferedImage(30, 30, BufferedImage.TYPE_3BYTE_BGR); - BufferedImage[] images = new BufferedImage[]{smallImage, largeImage}; - ImageCaptcha captcha = handler.generate(2, cipherHandler, dummySerializable, password, images, images, true); - BufferedImage[] resizedImages = Stream.of(captcha.getImgData()).map(data -> { - try (ByteArrayInputStream stream = new ByteArrayInputStream(data)) { - return ImageIO.read(stream); - } catch (IOException e) { - e.printStackTrace(); - return null; - } - }).toArray(BufferedImage[]::new); - assertTrue(captcha.getToken().length() > 0); - assertTrue(Stream.of(resizedImages).allMatch(img -> (img.getHeight() == 30 && img.getWidth() == 30))); - } - - @Test - public void autoResizeTest30x10() { - BufferedImage smallImage = new BufferedImage(10, 10, BufferedImage.TYPE_3BYTE_BGR); - BufferedImage largeImage = new BufferedImage(10, 30, BufferedImage.TYPE_3BYTE_BGR); - BufferedImage[] images = new BufferedImage[]{smallImage, largeImage}; - ImageCaptcha captcha = handler.generate(2, cipherHandler, dummySerializable, password, images, images, true); - BufferedImage[] resizedImages = Stream.of(captcha.getImgData()).map(data -> { - try (ByteArrayInputStream stream = new ByteArrayInputStream(data)) { - return ImageIO.read(stream); - } catch (IOException e) { - e.printStackTrace(); - return null; - } - }).toArray(BufferedImage[]::new); - assertTrue(captcha.getToken().length() > 0); - assertTrue(Stream.of(resizedImages).allMatch(img -> (img.getHeight() == 30 && img.getWidth() == 10))); - } - - @Test - public void autoResizeTest10x30() { - BufferedImage smallImage = new BufferedImage(10, 10, BufferedImage.TYPE_3BYTE_BGR); - BufferedImage largeImage = new BufferedImage(30, 10, BufferedImage.TYPE_3BYTE_BGR); - BufferedImage[] images = new BufferedImage[]{smallImage, largeImage}; - ImageCaptcha captcha = handler.generate(2, cipherHandler, dummySerializable, password, images, images, true); - BufferedImage[] resizedImages = Stream.of(captcha.getImgData()).map(data -> { - try (ByteArrayInputStream stream = new ByteArrayInputStream(data)) { - return ImageIO.read(stream); - } catch (IOException e) { - e.printStackTrace(); - return null; - } - }).toArray(BufferedImage[]::new); - assertTrue(captcha.getToken().length() > 0); - assertTrue(Stream.of(resizedImages).allMatch(img -> (img.getHeight() == 10 && img.getWidth() == 30))); - } - - private CipherHandler getCHMock() { - CipherHandler cipherHandler = Mockito.mock(CipherHandler.class); - Mockito.when(cipherHandler.generateIV()) - .thenReturn(new IvParameterSpec(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})); - Mockito.when(cipherHandler.decryptString(any(byte[].class), anyString(), any())) - .thenReturn(new byte[]{1, 2, 3}); - Mockito.when(cipherHandler.encryptString(any(byte[].class), anyString(), any(), any(byte[].class))) - .thenReturn(new byte[]{1, 2, 3}); - return cipherHandler; - } - -} diff --git a/src/test/java/io/github/yaforster/flexcaptcha/imgbased/handling/impl/SimpleImageCaptchaHandlerTest.java b/src/test/java/io/github/yaforster/flexcaptcha/imgbased/handling/impl/SimpleImageCaptchaHandlerTest.java deleted file mode 100644 index 71d1715..0000000 --- a/src/test/java/io/github/yaforster/flexcaptcha/imgbased/handling/impl/SimpleImageCaptchaHandlerTest.java +++ /dev/null @@ -1,111 +0,0 @@ -package io.github.yaforster.flexcaptcha.imgbased.handling.impl; - -import io.github.yaforster.flexcaptcha.CipherHandler; -import io.github.yaforster.flexcaptcha.imgbased.ImageCaptcha; -import io.github.yaforster.flexcaptcha.imgbased.handling.ImageCaptchaHandler; -import org.junit.Test; -import org.mockito.Mockito; - -import javax.crypto.spec.IvParameterSpec; -import javax.imageio.ImageIO; -import java.awt.*; -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.util.stream.Stream; - -import static org.junit.Assert.assertThrows; -import static org.junit.Assert.assertTrue; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; - -/** - * Tests {@link SimpleImageCaptchaHandler} - * - * @author Yannick Forster - */ -public class SimpleImageCaptchaHandlerTest { - - private final ImageCaptchaHandler handler = new SimpleImageCaptchaHandler(); - private final BufferedImage[] dummyArr = new BufferedImage[]{new BufferedImage(10, 10, BufferedImage.TYPE_3BYTE_BGR)}; - private final BufferedImage[] dummyArr2 = new BufferedImage[]{new BufferedImage(15, 15, BufferedImage.TYPE_4BYTE_ABGR)}; - private final Button dummySerializable = new Button(); - private final String password = "ThisIsMyPassword!"; - private final CipherHandler cipherHandler = getCHMock(); - - @Test - public void testAllNull() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(0, cipherHandler, null, null, null, null, 0, 0, true)); - } - - @Test - public void testGridWidth0_ShouldFail() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(0, cipherHandler, dummySerializable, password, - dummyArr, dummyArr2, true)); - } - - @Test - public void testGridWidth1_ShouldFail() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(1, cipherHandler, dummySerializable, password, - dummyArr, dummyArr2, true)); - } - - @Test - public void manualResizeTest30x10() { - BufferedImage smallImage = new BufferedImage(10, 10, BufferedImage.TYPE_3BYTE_BGR); - BufferedImage largeImage = new BufferedImage(10, 30, BufferedImage.TYPE_3BYTE_BGR); - BufferedImage[] images = new BufferedImage[]{smallImage, largeImage}; - ImageCaptcha captcha = handler.generate(2, cipherHandler, dummySerializable, - password, images, images, 30, 10, true); - BufferedImage[] resizedImages = getResizedImages(captcha); - assertTrue(captcha.getToken().length() > 0); - assertTrue(Stream.of(resizedImages).allMatch(img -> (img.getHeight() == 30 && img.getWidth() == 10))); - } - - @Test - public void manualResizeTest30x30() { - BufferedImage smallImage = new BufferedImage(10, 10, BufferedImage.TYPE_3BYTE_BGR); - BufferedImage largeImage = new BufferedImage(10, 30, BufferedImage.TYPE_3BYTE_BGR); - BufferedImage[] images = new BufferedImage[]{smallImage, largeImage}; - ImageCaptcha captcha = handler.generate(2, cipherHandler, dummySerializable, - password, images, images, 30, 30, true); - BufferedImage[] resizedImages = getResizedImages(captcha); - assertTrue(captcha.getToken().length() > 0); - assertTrue(Stream.of(resizedImages).allMatch(img -> (img.getHeight() == 30 && img.getWidth() == 30))); - } - - @Test - public void manualResizeTest10x30() { - BufferedImage smallImage = new BufferedImage(10, 10, BufferedImage.TYPE_3BYTE_BGR); - BufferedImage largeImage = new BufferedImage(10, 30, BufferedImage.TYPE_3BYTE_BGR); - BufferedImage[] images = new BufferedImage[]{smallImage, largeImage}; - ImageCaptcha captcha = handler.generate(2, cipherHandler, dummySerializable, - password, images, images, 10, 30, true); - BufferedImage[] resizedImages = getResizedImages(captcha); - assertTrue(captcha.getToken().length() > 0); - assertTrue(Stream.of(resizedImages).allMatch(img -> (img.getHeight() == 10 && img.getWidth() == 30))); - } - - private BufferedImage[] getResizedImages(ImageCaptcha captcha) { - return Stream.of(captcha.getImgData()).map(data -> { - try (ByteArrayInputStream stream = new ByteArrayInputStream(data)) { - return ImageIO.read(stream); - } catch (IOException e) { - e.printStackTrace(); - return null; - } - }).toArray(BufferedImage[]::new); - } - - private CipherHandler getCHMock() { - CipherHandler cipherHandler = Mockito.mock(CipherHandler.class); - Mockito.when(cipherHandler.generateIV()) - .thenReturn(new IvParameterSpec(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})); - Mockito.when(cipherHandler.decryptString(any(byte[].class), anyString(), any())) - .thenReturn(new byte[]{1, 2, 3}); - Mockito.when(cipherHandler.encryptString(any(byte[].class), anyString(), any(), any(byte[].class))) - .thenReturn(new byte[]{1, 2, 3}); - return cipherHandler; - } - -} diff --git a/src/test/java/io/github/yaforster/flexcaptcha/impl/rendering/BackgroundImageTest.java b/src/test/java/io/github/yaforster/flexcaptcha/impl/rendering/BackgroundImageTest.java new file mode 100644 index 0000000..466738d --- /dev/null +++ b/src/test/java/io/github/yaforster/flexcaptcha/impl/rendering/BackgroundImageTest.java @@ -0,0 +1,27 @@ +package io.github.yaforster.flexcaptcha.impl.rendering; + +import org.junit.jupiter.api.Test; + +import java.awt.*; +import java.awt.image.BufferedImage; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class BackgroundImageTest { + + @Test + void drawBackground_test_pixels_of_image() { + Color testColor = Color.RED; + BufferedImage fictionalImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + fictionalImage.setRGB(0, 0, testColor.getRGB()); + BackgroundImage background = new BackgroundImage(Color.BLUE, fictionalImage); + BufferedImage testImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + + background.drawBackground(testImage); + + int pixel = testImage.getRGB(0, 0); + Color pixelColor = new Color(pixel, true); + + assertEquals(testColor, pixelColor); + } +} \ No newline at end of file diff --git a/src/test/java/io/github/yaforster/flexcaptcha/impl/rendering/CaptchaRendererTest.java b/src/test/java/io/github/yaforster/flexcaptcha/impl/rendering/CaptchaRendererTest.java new file mode 100644 index 0000000..bb25970 --- /dev/null +++ b/src/test/java/io/github/yaforster/flexcaptcha/impl/rendering/CaptchaRendererTest.java @@ -0,0 +1,99 @@ +package io.github.yaforster.flexcaptcha.impl.rendering; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.OutputStream; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.any; + +class CaptchaRendererTest { + + @Test + final void renderAndConvertToBytes_with_default_renderer() { + CaptchaRenderer renderer = CaptchaRenderer.getDefaultCaptchaRenderer(); + assertDoesNotThrow(() -> renderer.renderAndConvertToBytes("abc123")); + } + + @Test + final void renderAndConvertToBytes_with_undefined_renderer() { + CaptchaRenderer renderer = CaptchaRenderer.builder().build(); + assertDoesNotThrow(() -> renderer.renderAndConvertToBytes("abc123")); + } + + @Test + final void renderAndConvertToBytes_test_height() throws IOException { + int targetHeight = 100; + CaptchaRenderer renderer = CaptchaRenderer.builder().pictureHeight(targetHeight).build(); + byte[] imageAsBytes = renderer.renderAndConvertToBytes("abc123"); + try (ByteArrayInputStream bis = new ByteArrayInputStream(imageAsBytes)) { + BufferedImage image = ImageIO.read(bis); + assertEquals(targetHeight, image.getHeight()); + } + } + + @Test + final void renderAndConvertToBytes_test_width() throws IOException { + int targetWidth = 300; + CaptchaRenderer renderer = CaptchaRenderer.builder().pictureWidth(targetWidth).build(); + byte[] imageAsBytes = renderer.renderAndConvertToBytes("abc123"); + try (ByteArrayInputStream bis = new ByteArrayInputStream(imageAsBytes)) { + BufferedImage image = ImageIO.read(bis); + assertEquals(targetWidth, image.getWidth()); + } + } + + @Test + final void renderAndConvertToBytes_test_color_settings() throws IOException { + CaptchaRenderer renderer = CaptchaRenderer.builder() + .noiseSettings(new NoiseSettings(1, Color.GRAY)) + .availableTextColors(Collections.singletonList(Color.BLUE)) + .imageBackground(new FlatColorBackground(Color.BLUE)) + .build(); + byte[] imageAsBytes = renderer.renderAndConvertToBytes("abc123"); + try (ByteArrayInputStream bis = new ByteArrayInputStream(imageAsBytes)) { + BufferedImage image = ImageIO.read(bis); + int pixel = image.getRGB(0, 0); + Color pixelColor = new Color(pixel, true); + assertTrue(200 < pixelColor.getBlue()); + } + } + + @Test + final void renderAndConvertToBytes_as_png() { + CaptchaRenderer renderer = CaptchaRenderer.builder().imgFileFormat("png").build(); + assertDoesNotThrow(() -> renderer.renderAndConvertToBytes("abc123")); + } + + @Test + final void renderAndConvertToBytes_catch_null_in_builder() { + CaptchaRenderer renderer = CaptchaRenderer.builder().imgFileFormat(null).build(); + assertDoesNotThrow(() -> renderer.renderAndConvertToBytes("abc123")); + } + + @Test + final void renderAndConvertToBytes_with_multiple_colors() { + List colors = List.of(Color.BLUE, Color.BLACK); + CaptchaRenderer renderer = CaptchaRenderer.builder().availableTextColors(colors).build(); + assertDoesNotThrow(() -> renderer.renderAndConvertToBytes("abc123")); + } + + @Test + final void renderAndConvertToBytes_test_errorhandling() { + try (MockedStatic imageIOMock = Mockito.mockStatic(ImageIO.class)) { + imageIOMock.when(() -> ImageIO.write(any(), any(), Mockito.any(OutputStream.class))) + .thenThrow(IOException.class); + CaptchaRenderer renderer = CaptchaRenderer.builder().imgFileFormat(null).build(); + assertThrows(CaptchaRenderingException.class, () -> renderer.renderAndConvertToBytes("abc123")); + } + } +} \ No newline at end of file diff --git a/src/test/java/io/github/yaforster/flexcaptcha/impl/rendering/FlatColorBackgroundTest.java b/src/test/java/io/github/yaforster/flexcaptcha/impl/rendering/FlatColorBackgroundTest.java new file mode 100644 index 0000000..934b0c8 --- /dev/null +++ b/src/test/java/io/github/yaforster/flexcaptcha/impl/rendering/FlatColorBackgroundTest.java @@ -0,0 +1,25 @@ +package io.github.yaforster.flexcaptcha.impl.rendering; + +import org.junit.jupiter.api.Test; + +import java.awt.*; +import java.awt.image.BufferedImage; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class FlatColorBackgroundTest { + + @Test + void drawBackground_test_pixels_of_background_color() { + Color testColor = Color.BLUE; + FlatColorBackground background = new FlatColorBackground(testColor); + BufferedImage testImage = new BufferedImage(1, 1, BufferedImage.TYPE_INT_RGB); + + background.drawBackground(testImage); + + int pixel = testImage.getRGB(0, 0); + Color pixelColor = new Color(pixel, true); + + assertEquals(testColor, pixelColor); + } +} \ No newline at end of file diff --git a/src/test/java/io/github/yaforster/flexcaptcha/impl/token/CaptchaCipherTest.java b/src/test/java/io/github/yaforster/flexcaptcha/impl/token/CaptchaCipherTest.java new file mode 100644 index 0000000..326cee2 --- /dev/null +++ b/src/test/java/io/github/yaforster/flexcaptcha/impl/token/CaptchaCipherTest.java @@ -0,0 +1,96 @@ +package io.github.yaforster.flexcaptcha.impl.token; + +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +import java.io.Serializable; +import java.util.Base64; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.Mockito.when; + +class CaptchaCipherTest { + + @Test + void generateToken_without_expiration_time() { + String solution = "abc123"; + Serializable salt = "SomeSalt"; + CaptchaCipher cipher = CaptchaCipher.builder().build(); + + String generatedToken = cipher.generateToken(solution, salt); + byte[] decodedToken = Base64.getDecoder().decode(generatedToken); + + assertEquals(32, decodedToken.length); + } + + @Test + void generateToken_expiration_time() { + ExpirationTimeSettings mockedExpirationTime = Mockito.mock(ExpirationTimeSettings.class); + when(mockedExpirationTime.getTime()).thenReturn(0L); + when(mockedExpirationTime.expirationTimeMillisOffset()).thenReturn(1000L); + String solution = "abc123"; + Serializable salt = "SomeSalt"; + CaptchaCipher cipher = CaptchaCipher.builder().expirationTimeSettings(mockedExpirationTime).build(); + + String generatedToken = cipher.generateToken(solution, salt); + byte[] decodedToken = Base64.getDecoder().decode(generatedToken); + + assertEquals(48, decodedToken.length); + } + + @Test + void generate_and_validate_Token_without_expiration_time() { + String solution = "abc123"; + Serializable salt = "SomeSalt"; + CaptchaCipher cipher = CaptchaCipher.builder().build(); + + String generatedToken = cipher.generateToken(solution, salt); + boolean validationResult = cipher.validateToken(generatedToken, salt, solution); + + assertTrue(validationResult); + } + + @Test + void generate_and_validate_Token_with_expiration_time() { + ExpirationTimeSettings mockedExpirationTime = Mockito.mock(ExpirationTimeSettings.class); + when(mockedExpirationTime.getTime()).thenReturn(0L); + when(mockedExpirationTime.expirationTimeMillisOffset()).thenReturn(1000L); + String solution = "abc123"; + Serializable salt = "SomeSalt"; + CaptchaCipher cipher = CaptchaCipher.builder().expirationTimeSettings(mockedExpirationTime).build(); + + String generatedToken = cipher.generateToken(solution, salt); + + when(mockedExpirationTime.getTime()).thenReturn(999L); + boolean validationResult = cipher.validateToken(generatedToken, salt, solution); + + assertTrue(validationResult); + } + + @Test + void generate_and_validate_Token_with_expired_token() { + ExpirationTimeSettings mockedExpirationTime = Mockito.mock(ExpirationTimeSettings.class); + when(mockedExpirationTime.getTime()).thenReturn(0L); + when(mockedExpirationTime.expirationTimeMillisOffset()).thenReturn(1000L); + String solution = "abc123"; + Serializable salt = "SomeSalt"; + CaptchaCipher cipher = CaptchaCipher.builder().expirationTimeSettings(mockedExpirationTime).build(); + + String generatedToken = cipher.generateToken(solution, salt); + + when(mockedExpirationTime.getTime()).thenReturn(1001L); + boolean validationResult = cipher.validateToken(generatedToken, salt, solution); + + assertFalse(validationResult); + } + + @Test + void generateToken_test_error_handling() { + CaptchaCipher cipher = CaptchaCipher.builder().build(); + + TokengenerationException exception = assertThrows(TokengenerationException.class, + () -> cipher.generateToken(null, null)); + assertTrue(exception.getLocalizedMessage().contains("Fatal error during cryptographic operation")); + } + +} \ No newline at end of file diff --git a/src/test/java/io/github/yaforster/flexcaptcha/textbased/handling/impl/SecureTextCaptchaHandlerTest.java b/src/test/java/io/github/yaforster/flexcaptcha/textbased/handling/impl/SecureTextCaptchaHandlerTest.java deleted file mode 100644 index ca0633d..0000000 --- a/src/test/java/io/github/yaforster/flexcaptcha/textbased/handling/impl/SecureTextCaptchaHandlerTest.java +++ /dev/null @@ -1,112 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.handling.impl; - -import io.github.yaforster.flexcaptcha.Captcha; -import io.github.yaforster.flexcaptcha.CipherHandler; -import io.github.yaforster.flexcaptcha.textbased.TextCaptcha; -import io.github.yaforster.flexcaptcha.textbased.rendering.impl.SimpleTextImageRenderer; -import io.github.yaforster.flexcaptcha.textbased.textgen.impl.SimpleCaptchaTextGenerator; -import org.junit.Test; - -import java.awt.*; - -import static org.junit.Assert.*; - -public class SecureTextCaptchaHandlerTest { - - private final SecureTextCaptchaHandler handler = new SecureTextCaptchaHandler(); - private final SimpleCaptchaTextGenerator generator = new SimpleCaptchaTextGenerator(); - private final SimpleTextImageRenderer renderer = new SimpleTextImageRenderer(); - private final CipherHandler cipherHandler = new CipherHandler(); - private final Button dummySerializable = new Button(); - private final String password = "ThisIsMyPassword!"; - - @Test - public void testGenerateGeneric() { - TextCaptcha captcha = handler.generate(10, cipherHandler, "ABC", password, generator, renderer, 60, 300, true); - assertTrue(captcha.getToken().length() > 0); - assertNotNull(captcha.getImgData()); - } - - @Test - public void testGenerateGenericEmptySalt() { - TextCaptcha captcha = handler.generate(10, cipherHandler, "", password, generator, renderer, 60, 300, true); - assertTrue(captcha.getToken().length() > 0); - assertNotNull(captcha.getImgData()); - } - - @Test - public void testGenerateGenericNullSalt() { - TextCaptcha captcha = handler.generate(10, cipherHandler, null, password, generator, renderer, 60, 300, true); - assertTrue(captcha.getToken().length() > 0); - assertNotNull(captcha.getImgData()); - } - - @Test - public void testGenerateGenericAllPixelMinimum() { - TextCaptcha captcha = handler.generate(1, cipherHandler, "", password, generator, renderer, 3, 1, true); - assertTrue(captcha.getToken().length() > 0); - assertNotNull(captcha.getImgData()); - } - - @Test - public void testGenerateGenericLengthZero() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(0, cipherHandler, "", password, generator, renderer, 1, 1, true)); - } - - @Test - public void testGenerateGenericLengthNegative() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(-1, cipherHandler, "", password, generator, renderer, 1, 1, true)); - } - - @Test - public void testGenerateGenericIllegalHeight() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(1, cipherHandler, "", password, generator, renderer, 1, 1, true)); - } - - @Test - public void testGenerateGenericNegativeHeight() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(1, cipherHandler, "", password, generator, renderer, -3, 1, true)); - } - - @Test - public void testGenerateGenericIllegalWidth() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(1, cipherHandler, "", password, generator, renderer, 3, 0, true)); - } - - @Test - public void testGenerateGenericNegativeWidth() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(1, cipherHandler, "", password, generator, renderer, 3, -1, true)); - } - - @Test - public void testGenerateGenericNull() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(10, cipherHandler, null, password, null, null, 60, 300, true)); - } - - @Test - public void testGenerateGenericShort() { - TextCaptcha captcha = handler.generate(5, cipherHandler, "ABC", password, generator, renderer, 60, 300, true); - assertTrue(captcha.getToken().length() > 0); - assertNotNull(captcha.getImgData()); - } - - @Test - public void testGenerateGenericWithDummyObj() { - TextCaptcha captcha = handler.generate(5, cipherHandler, dummySerializable, password, generator, renderer, 60, 300, true); - assertTrue(captcha.getToken().length() > 0); - assertNotNull(captcha.getImgData()); - } - - @Test - public void testValidateEmptyString() { - assertThrows(IllegalArgumentException.class, () -> handler.toCaptcha("", cipherHandler, dummySerializable, password, new SimpleTextImageRenderer(), 60, 300, true)); - } - - @Test - public void testValidation() { - String myText = "myText"; - Captcha captcha = handler.toCaptcha(myText, cipherHandler, dummySerializable, password, renderer, 100, 50, false); - assertTrue(handler.validate(myText, captcha.getToken(), cipherHandler, dummySerializable, password)); - } - -} diff --git a/src/test/java/io/github/yaforster/flexcaptcha/textbased/handling/impl/SimpleTextCaptchaHandlerTest.java b/src/test/java/io/github/yaforster/flexcaptcha/textbased/handling/impl/SimpleTextCaptchaHandlerTest.java deleted file mode 100644 index fb97e23..0000000 --- a/src/test/java/io/github/yaforster/flexcaptcha/textbased/handling/impl/SimpleTextCaptchaHandlerTest.java +++ /dev/null @@ -1,141 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.handling.impl; - -import io.github.yaforster.flexcaptcha.Captcha; -import io.github.yaforster.flexcaptcha.CipherHandler; -import io.github.yaforster.flexcaptcha.textbased.TextCaptcha; -import io.github.yaforster.flexcaptcha.textbased.rendering.impl.SimpleTextImageRenderer; -import io.github.yaforster.flexcaptcha.textbased.textgen.impl.SimpleCaptchaTextGenerator; -import org.junit.Test; -import org.mockito.Mockito; - -import javax.crypto.spec.IvParameterSpec; -import java.awt.*; - -import static org.junit.Assert.*; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyString; - -/** - * Tests {@link SimpleTextCaptchaHandler} - * - * @author Yannick Forster - */ - -public class SimpleTextCaptchaHandlerTest { - - private final SimpleTextCaptchaHandler handler = new SimpleTextCaptchaHandler(); - private final SimpleCaptchaTextGenerator generator = new SimpleCaptchaTextGenerator(); - private final SimpleTextImageRenderer renderer = new SimpleTextImageRenderer(); - private final CipherHandler cipherHandler = getCHMock(); - private final Button dummySerializable = new Button(); - private final String password = "ThisIsMyPassword!"; - - @Test - public void testGenerateGeneric() { - TextCaptcha captcha = handler.generate(10, cipherHandler, "ABC", password, generator, renderer, 60, 300, true); - assertTrue(captcha.getToken().length() > 0); - assertNotNull(captcha.getImgData()); - } - - @Test - public void testGenerateGenericEmptySalt() { - TextCaptcha captcha = handler.generate(10, cipherHandler, "", password, generator, renderer, 60, 300, true); - assertTrue(captcha.getToken().length() > 0); - assertNotNull(captcha.getImgData()); - } - - @Test - public void testGenerateGenericNullSalt() { - TextCaptcha captcha = handler.generate(10, cipherHandler, null, password, generator, renderer, 60, 300, true); - assertTrue(captcha.getToken().length() > 0); - assertNotNull(captcha.getImgData()); - } - - @Test - public void testGenerateGenericAllPixelMinimum() { - TextCaptcha captcha = handler.generate(1, cipherHandler, "", password, generator, renderer, 3, 1, true); - assertTrue(captcha.getToken().length() > 0); - assertNotNull(captcha.getImgData()); - } - - @Test - public void testGenerateGenericLengthZero() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(0, cipherHandler, "", password, generator, renderer, 1, 1, true)); - } - - @Test - public void testGenerateGenericLengthNegative() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(-1, cipherHandler, "", password, generator, renderer, 1, 1, true)); - } - - @Test - public void testGenerateGenericIllegalHeight() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(1, cipherHandler, "", password, generator, renderer, 1, 1, true)); - } - - @Test - public void testGenerateGenericNegativeHeight() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(1, cipherHandler, "", password, generator, renderer, -3, 1, true)); - } - - @Test - public void testGenerateGenericIllegalWidth() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(1, cipherHandler, "", password, generator, renderer, 3, 0, true)); - } - - @Test - public void testGenerateGenericNegativeWidth() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(1, cipherHandler, "", password, generator, renderer, 3, -1, true)); - } - - @Test - public void testGenerateGenericNull() { - assertThrows(IllegalArgumentException.class, () -> handler.generate(10, cipherHandler, null, password, null, null, 60, 300, true)); - } - - @Test - public void testGenerateGenericShort() { - TextCaptcha captcha = handler.generate(5, cipherHandler, "ABC", password, generator, renderer, 60, 300, true); - assertTrue(captcha.getToken().length() > 0); - assertNotNull(captcha.getImgData()); - } - - @Test - public void testGenerateGenericWithDummyObj() { - TextCaptcha captcha = handler.generate(5, cipherHandler, dummySerializable, password, generator, renderer, 60, 300, true); - assertTrue(captcha.getToken().length() > 0); - assertNotNull(captcha.getImgData()); - } - - @Test - public void testValidateEmptyString() { - assertThrows(IllegalArgumentException.class, () -> handler.toCaptcha("", cipherHandler, dummySerializable, password, new SimpleTextImageRenderer(), 60, 300, true)); - } - - @Test - public void testToCaptchaAndValidate() { - TextCaptcha captcha = handler.toCaptcha("TESTSTRING", cipherHandler, dummySerializable, password, new SimpleTextImageRenderer(), 60, 300, true); - assertTrue(captcha.getToken().length() > 0); - assertNotNull(captcha.getImgData()); - assertTrue(handler.validate("TESTSTRING", captcha.getToken(), cipherHandler, dummySerializable, password)); - } - - @Test - public void testValidation() { - String myText = "myText"; - Captcha captcha = handler.toCaptcha(myText, cipherHandler, dummySerializable, password, renderer, 100, 50, false); - assertTrue(handler.validate(myText, captcha.getToken(), cipherHandler, dummySerializable, password)); - } - - private CipherHandler getCHMock() { - CipherHandler cipherHandler = Mockito.mock(CipherHandler.class); - Mockito.when(cipherHandler.generateIV()) - .thenReturn(new IvParameterSpec(new byte[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16})); - Mockito.when(cipherHandler.decryptString(any(byte[].class), anyString(), any())) - .thenReturn(new byte[]{1, 2, 3}); - Mockito.when(cipherHandler.encryptString(any(byte[].class), anyString(), any(), any(byte[].class))) - .thenReturn(new byte[]{1, 2, 3}); - return cipherHandler; - } - -} diff --git a/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/TextImageRendererTest.java b/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/TextImageRendererTest.java deleted file mode 100644 index 6a65182..0000000 --- a/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/TextImageRendererTest.java +++ /dev/null @@ -1,37 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.rendering; - -import lombok.Getter; -import org.junit.Before; -import org.junit.Test; - -import java.awt.image.BufferedImage; - -import static org.junit.Assert.assertEquals; - -/** - * Tests {@link TextImageRenderer} interface - * - * @author Yannick Forster - */ -@Getter -public abstract class TextImageRendererTest { - - private T renderer; - - protected abstract T createRenderer(); - - @Before - public void setUp() { - renderer = createRenderer(); - } - - @Test - public void render() { - int targetHeight = 100; - int widthHeight = 200; - BufferedImage image = renderer.render("TEST", targetHeight, widthHeight); - assertEquals(image.getHeight(), targetHeight); - assertEquals(image.getWidth(), widthHeight); - } - -} diff --git a/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/CleanTextImageRendererTest.java b/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/CleanTextImageRendererTest.java deleted file mode 100644 index a28a655..0000000 --- a/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/CleanTextImageRendererTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.rendering.impl; - - -import io.github.yaforster.flexcaptcha.textbased.rendering.TextImageRendererTest; -import org.junit.Test; - -public class CleanTextImageRendererTest extends TextImageRendererTest { - - @Test - public void testRender() { - render(); - } - - protected CleanTextImageRenderer createRenderer() { - return new CleanTextImageRenderer(); - } - -} diff --git a/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/EffectChainTextImageRendererTest.java b/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/EffectChainTextImageRendererTest.java deleted file mode 100644 index 0202d8a..0000000 --- a/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/EffectChainTextImageRendererTest.java +++ /dev/null @@ -1,19 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.rendering.impl; - -import io.github.yaforster.flexcaptcha.textbased.rendering.TextImageRendererTest; -import org.junit.Test; - -//TODO: Implement testing effect chaining -public class EffectChainTextImageRendererTest extends TextImageRendererTest { - - @Test - public void testRender() { - render(); - } - - @Override - protected EffectChainTextImageRenderer createRenderer() { - return new EffectChainTextImageRenderer(); - } - -} diff --git a/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/SimpleTextImageRendererTest.java b/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/SimpleTextImageRendererTest.java deleted file mode 100644 index 716aadb..0000000 --- a/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/SimpleTextImageRendererTest.java +++ /dev/null @@ -1,24 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.rendering.impl; - -import io.github.yaforster.flexcaptcha.textbased.rendering.TextImageRendererTest; -import org.junit.Test; - -public class SimpleTextImageRendererTest extends TextImageRendererTest { - - @Test - public void testRender() { - render(); - } - - @Test - public void renderWithNonExistantFont_ShouldWork() { - getRenderer().setFontName("DoesntExist"); - render(); - } - - @Override - protected SimpleTextImageRenderer createRenderer() { - return new SimpleTextImageRenderer(); - } - -} diff --git a/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/TwirledTextImageRendererTest.java b/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/TwirledTextImageRendererTest.java deleted file mode 100644 index d7a94d7..0000000 --- a/src/test/java/io/github/yaforster/flexcaptcha/textbased/rendering/impl/TwirledTextImageRendererTest.java +++ /dev/null @@ -1,18 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.rendering.impl; - -import io.github.yaforster.flexcaptcha.textbased.rendering.TextImageRendererTest; -import org.junit.Test; - -public class TwirledTextImageRendererTest extends TextImageRendererTest { - - @Test - public void testRender() { - render(); - } - - @Override - protected TwirledTextImageRenderer createRenderer() { - return new TwirledTextImageRenderer(); - } - -} diff --git a/src/test/java/io/github/yaforster/flexcaptcha/textbased/textgen/impl/SimpleCaptchaTextGeneratorTest.java b/src/test/java/io/github/yaforster/flexcaptcha/textbased/textgen/impl/SimpleCaptchaTextGeneratorTest.java deleted file mode 100644 index f235faa..0000000 --- a/src/test/java/io/github/yaforster/flexcaptcha/textbased/textgen/impl/SimpleCaptchaTextGeneratorTest.java +++ /dev/null @@ -1,108 +0,0 @@ -package io.github.yaforster.flexcaptcha.textbased.textgen.impl; - -import io.github.yaforster.flexcaptcha.textbased.enums.Case; -import org.junit.Before; -import org.junit.Test; - -import java.util.Locale; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; - -/** - * Tests {@link SimpleCaptchaTextGenerator} - * - * @author Yannick Forster - */ -public class SimpleCaptchaTextGeneratorTest { - static final int TESTSTRINGLENGTH10 = 10; - static final int TESTSTRINGLENGTH5 = 5; - private SimpleCaptchaTextGenerator generator; - - @Before - public void init() { - generator = new SimpleCaptchaTextGenerator(); - } - - @Test - public void testGetCaptchaStringInt() { - String s = generator.generate(TESTSTRINGLENGTH10); - assertEquals("CaptchaString is not of specified length.", TESTSTRINGLENGTH10, s.length()); - } - - @Test - public void testGetCaptchaStringInt5() { - String s = generator.generate(TESTSTRINGLENGTH5); - assertEquals("CaptchaString is not of specified length.", TESTSTRINGLENGTH5, s.length()); - } - - @Test - public void testGetCaptchaStringIntLowerCase() { - String s = generator.generate(TESTSTRINGLENGTH10, Case.LOWERCASE); - assertEquals("CaptchaString is not of specified length.", TESTSTRINGLENGTH10, s.length()); - assertEquals("CaptchaString is not lowercase", s, s.toLowerCase(Locale.ROOT)); - } - - @Test - public void testGetCaptchaStringIntUpperCase() { - String s = generator.generate(TESTSTRINGLENGTH10, Case.UPPERCASE); - assertEquals("CaptchaString is not of specified length.", TESTSTRINGLENGTH10, s.length()); - assertEquals("CaptchaString is not uppercase", s, s.toUpperCase(Locale.ROOT)); - } - - @Test - public void testGetCaptchaStringIntStringCaseNumOnly() { - String s = generator.generate(TESTSTRINGLENGTH10, "123456789"); - assertEquals("CaptchaString is not of specified length.", TESTSTRINGLENGTH10, s.length()); - assertTrue("CaptchaString is not a number", s.chars().allMatch(Character::isDigit)); - } - - @Test - public void testGetCaptchaStringIntStringCaseLetterOnly() { - String s = generator.generate(TESTSTRINGLENGTH10, "abcdefghijklmnopqrstuvwxyz"); - assertEquals("CaptchaString is not of specified length.", TESTSTRINGLENGTH10, s.length()); - assertTrue("CaptchaString is not a letter", s.chars().allMatch(Character::isLetter)); - } - - @Test - public void testGetCaptchaStringIntStringCaseNumOnlyMixedCase() { - String s = generator.generate(TESTSTRINGLENGTH10, "123456789", Case.MIXEDCASE); - assertEquals("CaptchaString is not of specified length.", TESTSTRINGLENGTH10, s.length()); - assertTrue("CaptchaString is not a number", s.chars().allMatch(Character::isDigit)); - } - - @Test - public void testGetCaptchaStringIntStringCaseLetterOnlyMixedCase() { - String s = generator.generate(TESTSTRINGLENGTH10, "abcdefghijklmnopqrstuvwxyz", Case.MIXEDCASE); - assertEquals("CaptchaString is not of specified length.", TESTSTRINGLENGTH10, s.length()); - assertTrue("CaptchaString is not a letter", s.chars().allMatch(Character::isLetter)); - } - - @Test - public void testGetCaptchaStringMixedStringMixedCase() { - String s = generator.generate(TESTSTRINGLENGTH10, "abcdefghijklmnopqrstuvwxyz1234567890", - Case.MIXEDCASE); - assertEquals("CaptchaString is not of specified length.", TESTSTRINGLENGTH10, s.length()); - assertTrue("CaptchaString is not a letter or a number", - s.chars().allMatch(i -> (Character.isLetter(i) || Character.isDigit(i)))); - } - - @Test - public void testGetCaptchaStringMixedStringUpperCase() { - String s = generator.generate(TESTSTRINGLENGTH10, "abcdefghijklmnopqrstuvwxyz1234567890", - Case.UPPERCASE); - assertEquals("CaptchaString is not of specified length.", TESTSTRINGLENGTH10, s.length()); - assertTrue("CaptchaString contains character or case that should not appear with the given configuration.", - s.chars().allMatch(i -> ((Character.isLetter(i) && Character.isUpperCase(i)) || Character.isDigit(i)))); - } - - @Test - public void testGetCaptchaStringMixedStringLowerCase() { - String s = generator.generate(TESTSTRINGLENGTH10, "abcdefghijklmnopqrstuvwxyz1234567890", - Case.LOWERCASE); - assertEquals("CaptchaString is not of specified length.", TESTSTRINGLENGTH10, s.length()); - assertTrue("CaptchaString contains character or case that should not appear with the given configuration.", - s.chars().allMatch(i -> ((Character.isLetter(i) && Character.isLowerCase(i)) || Character.isDigit(i)))); - } - -} diff --git a/src/test/java/io/github/yaforster/flexcaptcha/util/TextGeneratorTest.java b/src/test/java/io/github/yaforster/flexcaptcha/util/TextGeneratorTest.java new file mode 100644 index 0000000..ad126c4 --- /dev/null +++ b/src/test/java/io/github/yaforster/flexcaptcha/util/TextGeneratorTest.java @@ -0,0 +1,102 @@ +package io.github.yaforster.flexcaptcha.util; + +import org.junit.jupiter.api.Test; + +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class TextGeneratorTest { + + static final int TESTSTRINGLENGTH10 = 10; + static final int TESTSTRINGLENGTH5 = 5; + + @Test + public final void testGetCaptchaStringInt() { + TextGenerator generator = TextGenerator.builder().build(); + String s = generator.generate(TESTSTRINGLENGTH10); + assertEquals(TESTSTRINGLENGTH10, s.length()); + } + + @Test + public final void testGetCaptchaStringInt5() { + TextGenerator generator = TextGenerator.builder().build(); + String s = generator.generate(TESTSTRINGLENGTH5); + assertEquals(TESTSTRINGLENGTH5, s.length()); + } + + @Test + public final void testGetCaptchaStringIntLowerCase() { + TextGenerator generator = TextGenerator.builder().build(); + String s = generator.generate(TESTSTRINGLENGTH10, TextCase.LOWERCASE); + assertEquals(TESTSTRINGLENGTH10, s.length()); + assertEquals(s, s.toLowerCase(Locale.ROOT)); + } + + @Test + public final void testGetCaptchaStringIntUpperCase() { + TextGenerator generator = TextGenerator.builder().build(); + String s = generator.generate(TESTSTRINGLENGTH10, TextCase.UPPERCASE); + assertEquals(TESTSTRINGLENGTH10, s.length()); + assertEquals(s, s.toUpperCase(Locale.ROOT)); + } + + @Test + public final void testGetCaptchaStringIntStringCaseNumOnly() { + TextGenerator generator = TextGenerator.builder().characterbase("1234567890").build(); + String s = generator.generate(TESTSTRINGLENGTH10); + assertEquals(TESTSTRINGLENGTH10, s.length()); + assertTrue(s.chars().allMatch(Character::isDigit)); + } + + @Test + public final void testGetCaptchaStringIntStringCaseLetterOnly() { + TextGenerator generator = TextGenerator.builder().characterbase("abcdefg").build(); + String s = generator.generate(TESTSTRINGLENGTH10); + assertEquals(TESTSTRINGLENGTH10, s.length()); + assertTrue(s.chars().allMatch(Character::isLetter)); + } + + @Test + public final void testGetCaptchaStringIntStringCaseNumOnlyMixedCase() { + TextGenerator generator = TextGenerator.builder().characterbase("1234567890").build(); + String s = generator.generate(TESTSTRINGLENGTH10, TextCase.MIXEDCASE); + assertEquals(TESTSTRINGLENGTH10, s.length()); + assertTrue(s.chars().allMatch(Character::isDigit)); + } + + @Test + public final void testGetCaptchaStringIntStringCaseLetterOnlyMixedCase() { + TextGenerator generator = TextGenerator.builder().characterbase("abcdefg").build(); + String s = generator.generate(TESTSTRINGLENGTH10, TextCase.MIXEDCASE); + assertEquals(TESTSTRINGLENGTH10, s.length()); + assertTrue(s.chars().allMatch(Character::isLetter)); + } + + @Test + public final void testGetCaptchaStringMixedStringMixedCase() { + TextGenerator generator = TextGenerator.builder().characterbase("abcdefg123456").build(); + String s = generator.generate(TESTSTRINGLENGTH10, TextCase.MIXEDCASE); + assertEquals(TESTSTRINGLENGTH10, s.length()); + assertTrue(s.chars().allMatch(i -> (Character.isLetter(i) || Character.isDigit(i)))); + } + + @Test + public final void testGetCaptchaStringMixedStringUpperCase() { + TextGenerator generator = TextGenerator.builder().characterbase("abcdefg123456").build(); + String s = generator.generate(TESTSTRINGLENGTH10, TextCase.UPPERCASE); + assertEquals(TESTSTRINGLENGTH10, s.length()); + assertTrue(s.chars() + .allMatch(i -> ((Character.isLetter(i) && Character.isUpperCase(i)) || Character.isDigit(i)))); + } + + @Test + public final void testGetCaptchaStringMixedStringLowerCase() { + TextGenerator generator = TextGenerator.builder().characterbase("abcdefg123456").build(); + String s = generator.generate(TESTSTRINGLENGTH10, TextCase.LOWERCASE); + assertEquals(TESTSTRINGLENGTH10, s.length()); + assertTrue(s.chars() + .allMatch(i -> ((Character.isLetter(i) && Character.isLowerCase(i)) || Character.isDigit(i)))); + } +} \ No newline at end of file