diff --git a/norconex-commons-lang/pom.xml b/norconex-commons-lang/pom.xml index 06e2fa1e..f02b2043 100644 --- a/norconex-commons-lang/pom.xml +++ b/norconex-commons-lang/pom.xml @@ -19,7 +19,7 @@ 4.0.0 com.norconex.commons norconex-commons-lang - 1.14.0 + 1.15.0 jar Norconex Commons Lang @@ -27,7 +27,7 @@ UTF-8 UTF-8 - 1.14.0 + 1.15.0 3.6 1.10 diff --git a/norconex-commons-lang/src/changes/changes.xml b/norconex-commons-lang/src/changes/changes.xml index 00766a0e..eb513103 100644 --- a/norconex-commons-lang/src/changes/changes.xml +++ b/norconex-commons-lang/src/changes/changes.xml @@ -7,6 +7,26 @@ + + + New EncryptionXMLUtil class offering methods to facilitate integration + of EncryptionKey with IXMLConfigurable objects (or other XML objects). + + + EncryptionUtil now uses AES for encryption and supports custom key size. + + + ConfigurationLoader now sets the Velocity character encoding to UTF-8. + + + HttpURL now extract protocols before first colon, no longer requiring + two forward slash. Also more lenient towards relative URLs. + + + QueryString now strips out fragments when part of a URL. + + + Can now store and load Properties file as JSON. diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/ClassFinder.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/ClassFinder.java index c779ea8b..85aeaa9d 100644 --- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/ClassFinder.java +++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/ClassFinder.java @@ -1,4 +1,4 @@ -/* Copyright 2010-2016 Norconex Inc. +/* Copyright 2010-2018 Norconex Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -42,7 +42,6 @@ * * @author Pascal Essiembre */ -@SuppressWarnings("nls") public final class ClassFinder { private static final Logger LOG = Logger.getLogger(ClassFinder.class); @@ -127,8 +126,7 @@ public static List findSubTypes( return new ArrayList<>(); } if (file.isDirectory()) { - return findSubTypesFromDirectory( - new File(file.getAbsolutePath() + "/"), superClass); + return findSubTypesFromDirectory(file, superClass); } if (file.getName().endsWith(".jar")) { return findSubTypesFromJar(file, superClass); diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/Sleeper.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/Sleeper.java index 877df953..3942eb5b 100644 --- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/Sleeper.java +++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/Sleeper.java @@ -1,4 +1,4 @@ -/* Copyright 2010-2014 Norconex Inc. +/* Copyright 2010-2018 Norconex Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -23,7 +23,6 @@ * * @author Pascal Essiembre */ -@SuppressWarnings("nls") public final class Sleeper { /** Number of milliseconds representing 1 second. */ diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/StringUtil.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/StringUtil.java index c4d44940..719d58dc 100644 --- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/StringUtil.java +++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/StringUtil.java @@ -1,4 +1,4 @@ -/* Copyright 2017 Norconex Inc. +/* Copyright 2017-2018 Norconex Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -110,9 +110,11 @@ public static String truncateWithHash( * For this reason, the maxByteLength argument must be * be large enough for any truncation to occur. * @param text text to truncate + * @param charset character encoding * @param maxByteLength maximum byte length the truncated text must have * @return truncated character byte array, or original text if no * truncation required + * @throws CharacterCodingException character coding problem */ public static String truncateBytesWithHash(String text, Charset charset, int maxByteLength) @@ -135,10 +137,12 @@ public static String truncateBytesWithHash(String text, * For this reason, the maxByteLength argument must be * be large enough for any truncation to occur. * @param text text to truncate + * @param charset character encoding * @param maxByteLength maximum byte length the truncated text must have * @param separator string separating truncated text from hash code * @return truncated character byte array, or original text if no * truncation required + * @throws CharacterCodingException character coding problem */ public static String truncateBytesWithHash(String text, Charset charset, int maxByteLength, String separator) @@ -161,9 +165,11 @@ public static String truncateBytesWithHash(String text, * For this reason, the maxByteLength argument must be * be large enough for any truncation to occur. * @param bytes byte array of text to truncate + * @param charset character encoding * @param maxByteLength maximum byte length the truncated text must have * @return truncated character byte array, or original text if no * truncation required + * @throws CharacterCodingException character coding problem */ public static byte[] truncateBytesWithHash( byte[] bytes, Charset charset, int maxByteLength) @@ -184,10 +190,12 @@ public static byte[] truncateBytesWithHash( * For this reason, the maxByteLength argument must be * be large enough for any truncation to occur. * @param bytes byte array of text to truncate + * @param charset character encoding * @param maxByteLength maximum byte length the truncated text must have * @param separator string separating truncated text from hash code * @return truncated character byte array, or original text if no * truncation required + * @throws CharacterCodingException character coding problem */ public static byte[] truncateBytesWithHash( byte[] bytes, Charset charset, int maxByteLength, String separator) diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/ConfigurationLoader.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/ConfigurationLoader.java index 5013a422..9087856d 100644 --- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/ConfigurationLoader.java +++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/config/ConfigurationLoader.java @@ -121,7 +121,6 @@ *

* @author Pascal Essiembre */ -@SuppressWarnings("nls") public final class ConfigurationLoader { private static final String EXTENSION_PROPERTIES = ".properties"; @@ -139,6 +138,8 @@ public ConfigurationLoader() { velocityEngine.setProperty(RuntimeConstants.RESOURCE_LOADER, "file"); velocityEngine.setProperty( RuntimeConstants.FILE_RESOURCE_LOADER_PATH, ""); + velocityEngine.setProperty(RuntimeConstants.INPUT_ENCODING, "UTF-8"); + velocityEngine.setProperty(RuntimeConstants.OUTPUT_ENCODING, "UTF-8"); velocityEngine.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS, "org.apache.velocity.runtime.log.Log4JLogChute"); velocityEngine.setProperty("runtime.log", ""); diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionKey.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionKey.java index e8cc98bc..e7f980ae 100644 --- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionKey.java +++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionKey.java @@ -1,4 +1,4 @@ -/* Copyright 2015-2016 Norconex Inc. +/* Copyright 2015-2018 Norconex Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -22,8 +22,8 @@ import java.nio.file.Paths; /** - * Pointer to the an encryption key, or the encryption key itself. An - * encryption key can be seen as equivalent to a secret key, + * Pointer to the an encryption key, or the encryption key itself. An + * encryption key can be seen as equivalent to a secret key, * passphrase or password. * @author Pascal Essiembre * @since 1.9.0 @@ -33,38 +33,66 @@ public class EncryptionKey implements Serializable { private static final long serialVersionUID = 1L; - public enum Source { + public static final int DEFAULT_KEY_SIZE = 128; + + public enum Source { /** Value is the actual key. */ - KEY, + KEY, /** Value is the path to a file containing the key. */ - FILE, + FILE, /** Value is the name of an environment variable containing the key. */ - ENVIRONMENT, + ENVIRONMENT, /** Value is the name of a JVM system property containing the key. */ PROPERTY } private final String value; + private final Integer size; private final Source source; - + /** - * Creates a new reference to an encryption key. The reference can either + * Creates a new reference to an encryption key. The reference can either * be the key itself, or a pointer to a file or environment variable - * containing the key (as defined by the supplied value type). + * containing the key (as defined by the supplied value type). The actual + * value can be any sort of string, and it is converted to an encryption + * key of length size using cryptographic algorithms. If the size is + * specified, it must be supported by your version of Java. + * * @param value the encryption key + * @param size the size in bits of the encryption key * @param source the type of value */ - public EncryptionKey(String value, Source source) { + public EncryptionKey(String value, Source source, int size) { super(); this.value = value; this.source = source; + this.size = size; + } + /** + * Creates a new reference to an encryption key. The reference can either + * be the key itself, or a pointer to a file or environment variable + * containing the key (as defined by the supplied value type). + * @param value the encryption key + * @param source the type of value + */ + public EncryptionKey(String value, Source source) { + this(value, source, DEFAULT_KEY_SIZE); + } + /** + * Creates a new encryption key where the value is the actual key, and the + * number of key bits to generate is the size. + * @param value the encrption key + * @param size the encryption key size in bits + */ + public EncryptionKey(String value, int size) { + this(value, Source.KEY, size); } /** * Creates a new encryption key where the value is the actual key. * @param value the encryption key */ public EncryptionKey(String value) { - this(value, Source.KEY); + this(value, Source.KEY, DEFAULT_KEY_SIZE); } public String getValue() { return value; @@ -72,13 +100,22 @@ public String getValue() { public Source getSource() { return source; } + /** + * Gets the size in bits of the encryption key. Default is + * {@value #DEFAULT_KEY_SIZE}. + * @return size in bits of the encryption key + * @since 1.15.0 + */ + public int getSize() { + return (size != null ? size : DEFAULT_KEY_SIZE); + } /** - * Locate the key according to its value type and return it. This - * method will always resolve the value each type it is invoked and + * Locate the key according to its value type and return it. This + * method will always resolve the value each type it is invoked and * never caches the key, unless the key value specified at construction * time is the actual key. - * @return encryption key or null if the key does not exist + * @return encryption key or null if the key does not exist * for the specified type */ public String resolve() { @@ -101,12 +138,12 @@ public String resolve() { return null; } } - + private String fromEnv() { //TODO allow a flag to optionally throw an exception when null? return System.getenv(value); } - + private String fromProperty() { //TODO allow a flag to optionally throw an exception when null? return System.getProperty(value); @@ -128,7 +165,7 @@ private String fromFile() { "Could not read key file.", e); } } - + //Do not use Apache Commons Lang below to avoid any dependency //when used on command-line with EncryptionUtil. @Override @@ -137,6 +174,7 @@ public int hashCode() { int result = 1; result = prime * result + ((source == null) ? 0 : source.hashCode()); result = prime * result + ((value == null) ? 0 : value.hashCode()); + result = prime * result + size; return result; } @Override @@ -161,10 +199,18 @@ public boolean equals(Object obj) { } else if (!value.equals(other.value)) { return false; } + if (size == null) { + if (other.size != null) { + return false; + } + } else if (!size.equals(other.size)) { + return false; + } return true; } @Override public String toString() { - return "EncryptionKey [value=" + value + ", source=" + source + "]"; - } + return "EncryptionKey [value=" + value + + ", source=" + source + ", size=" + size + "]"; + } } diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionUtil.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionUtil.java index df321c5e..710fa2ea 100644 --- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionUtil.java +++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionUtil.java @@ -1,4 +1,4 @@ -/* Copyright 2015-2017 Norconex Inc. +/* Copyright 2015-2018 Norconex Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,27 +14,36 @@ */ package com.norconex.commons.lang.encrypt; +import java.io.ByteArrayOutputStream; import java.io.PrintStream; import java.nio.charset.StandardCharsets; +import java.security.AlgorithmParameters; +import java.security.GeneralSecurityException; import java.security.spec.AlgorithmParameterSpec; import java.security.spec.KeySpec; +import java.util.Arrays; import javax.crypto.Cipher; import javax.crypto.SecretKey; import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.IvParameterSpec; import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.PBEParameterSpec; +import javax.crypto.spec.SecretKeySpec; import javax.xml.bind.DatatypeConverter; import com.norconex.commons.lang.encrypt.EncryptionKey.Source; /** - *

Simplified encryption and decryption methods using the - * "PBEWithMD5AndDES" algorithm with a supplied encryption key (which you - * can also think of as a passphrase, or password). - * The "salt" and iteration count used by this class are hard-coded. To have - * more control and ensure a more secure approach, you should rely on another - * implementation or create your own. + *

Simplified encryption and decryption methods using the + * + * Advanced Encryption Standard (AES) (since 1.15.0) with a supplied + * encryption key (which you can also think of as a passphrase, or password). + *

+ *

+ * The "salt" and iteration count used by this class are hard-coded. To use + * a different encryption or have more control over its creation, + * you should rely on another implementation or create your own. *

*

* To use on the command prompt, use the following command to print usage @@ -44,7 +53,7 @@ * java -cp norconex-commons-lang-[version].jar com.norconex.commons.lang.encrypt.EncryptionUtil * *

- * For example, to use a encryption key store in a file to encrypt some text, + * For example, to use a encryption key store in a file to encrypt some text, * add the following arguments to the above command: *

*
@@ -54,12 +63,12 @@
  * As of 1.13.0, you can also use the encrypt.[sh|bat] and
  * decrypt.[sh|bat] files distributed with this library.
  * 

- * + * * @author Pascal Essiembre * @since 1.9.0 */ public class EncryptionUtil { - + private EncryptionUtil() { super(); } @@ -72,7 +81,7 @@ public static void main(String[] args) { String typeArg = args[1]; String keyArg = args[2]; String textArg = args[3]; - + Source type = null; if ("-k".equalsIgnoreCase(typeArg)) { type = Source.KEY; @@ -86,7 +95,7 @@ public static void main(String[] args) { System.err.println("Unsupported type of key: " + type); printUsage(); } - + EncryptionKey key = new EncryptionKey(keyArg, type); if ("encrypt".equalsIgnoreCase(cmdArg)) { System.out.println(encrypt(textArg, key)); @@ -116,15 +125,15 @@ private static void printUsage() { out.println(" text text to encrypt or decrypt"); System.exit(-1); } - + /** *

Encrypts the given text with the encryption key supplied. If the * encryption key is null or resolves to blank key, * the text to encrypt will be returned unmodified.

* @param textToEncrypt text to be encrypted - * @param encryptionKey encryption key which must resolve to the same + * @param encryptionKey encryption key which must resolve to the same * value to encrypt and decrypt the supplied text. - * @return encrypted text or null if + * @return encrypted text or null if * textToEncrypt is null. */ public static String encrypt( @@ -139,49 +148,56 @@ public static String encrypt( if (key == null) { return textToEncrypt; } - + // 8-byte Salt byte[] salt = { - (byte)0xE3, (byte)0x03, (byte)0x9B, (byte)0xA9, + (byte)0xE3, (byte)0x03, (byte)0x9B, (byte)0xA9, (byte)0xC8, (byte)0x16, (byte)0x35, (byte)0x56 }; // Iteration count - int iterationCount = 19; + int iterationCount = 1000; + int keySize = encryptionKey.getSize(); Cipher ecipher; try { // Create the key KeySpec keySpec = new PBEKeySpec( - key.trim().toCharArray(), salt, iterationCount); - SecretKey secretKey = SecretKeyFactory.getInstance( - "PBEWithMD5AndDES").generateSecret(keySpec); - ecipher = Cipher.getInstance(secretKey.getAlgorithm()); - - // Prepare the parameter to the ciphers - AlgorithmParameterSpec paramSpec = - new PBEParameterSpec(salt, iterationCount); - - // Create the ciphers - ecipher.init(Cipher.ENCRYPT_MODE, secretKey, paramSpec); - + key.trim().toCharArray(), salt, iterationCount, keySize); + SecretKeyFactory factory = + SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + + SecretKey secretKeyTemp = factory.generateSecret(keySpec); + SecretKey secretKey = + new SecretKeySpec(secretKeyTemp.getEncoded(), "AES"); + + ecipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + ecipher.init(Cipher.ENCRYPT_MODE, secretKey); + + AlgorithmParameters params = ecipher.getParameters(); + + byte[] iv = params.getParameterSpec(IvParameterSpec.class).getIV(); byte[] utf8 = textToEncrypt.trim().getBytes(StandardCharsets.UTF_8); - byte[] enc = ecipher.doFinal(utf8); - - return DatatypeConverter.printBase64Binary(enc); + byte[] cipherBytes = ecipher.doFinal(utf8); + + try (ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + bos.write(iv); + bos.write(cipherBytes); + return DatatypeConverter.printBase64Binary(bos.toByteArray()); + } } catch (Exception e) { throw new EncryptionException("Encryption failed.", e); } } - + /** *

Decrypts the given encrypted text with the encryption key supplied. *

* @param encryptedText text to be decrypted - * @param encryptionKey encryption key which must resolve to the same + * @param encryptionKey encryption key which must resolve to the same * value to encrypt and decrypt the supplied text. - * @return decrypted text or null if one of + * @return decrypted text or null if one of * encryptedText or key is null. - */ + */ public static String decrypt( String encryptedText, EncryptionKey encryptionKey) { if (encryptedText == null) { @@ -193,38 +209,79 @@ public static String decrypt( String key = encryptionKey.resolve(); if (key == null) { return encryptedText; - } - + } + // 8-byte Salt byte[] salt = { - (byte)0xE3, (byte)0x03, (byte)0x9B, (byte)0xA9, + (byte)0xE3, (byte)0x03, (byte)0x9B, (byte)0xA9, (byte)0xC8, (byte)0x16, (byte)0x35, (byte)0x56 }; // Iteration count - int iterationCount = 19; + int iterationCount = 1000; + int keySize = encryptionKey.getSize(); Cipher dcipher; try { + // Separate the encrypted data into the salt and the + // encrypted message + byte[] cryptMessage = + DatatypeConverter.parseBase64Binary(encryptedText.trim()); + byte[] iv = Arrays.copyOf(cryptMessage, 16); + byte[] cryptBytes = Arrays.copyOfRange( + cryptMessage, 16, cryptMessage.length); + // Create the key KeySpec keySpec = new PBEKeySpec( - key.trim().toCharArray(), salt, iterationCount); - SecretKey secretKey = SecretKeyFactory.getInstance( - "PBEWithMD5AndDES").generateSecret(keySpec); - dcipher = Cipher.getInstance(secretKey.getAlgorithm()); - - // Prepare the parameter to the ciphers - AlgorithmParameterSpec paramSpec = - new PBEParameterSpec(salt, iterationCount); - - // Create the ciphers - dcipher.init(Cipher.DECRYPT_MODE, secretKey, paramSpec); - - byte[] dec = - DatatypeConverter.parseBase64Binary(encryptedText.trim()); - byte[] utf8 = dcipher.doFinal(dec); + key.trim().toCharArray(), salt, iterationCount, keySize); + SecretKeyFactory factory = + SecretKeyFactory.getInstance("PBKDF2WithHmacSHA1"); + SecretKey secretKeyTemp = factory.generateSecret(keySpec); + SecretKey secretKey = + new SecretKeySpec(secretKeyTemp.getEncoded(), "AES"); + + IvParameterSpec ivParamSpec = new IvParameterSpec(iv); + + dcipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + dcipher.init(Cipher.DECRYPT_MODE, secretKey, ivParamSpec); + + byte[] utf8 = dcipher.doFinal(cryptBytes); return new String(utf8, StandardCharsets.UTF_8); - } catch (Exception e) { - throw new EncryptionException("Decryption failed.", e); + } catch (Exception original) { + try { + // Support for text encrypted before version 1.15.0. + return decryptLegacy(encryptedText, key); + } catch (GeneralSecurityException subsequent) { + throw new EncryptionException("Decryption failed.", original); + } } } + + private static String decryptLegacy(String encryptedText, String key) + throws GeneralSecurityException { + // 8-byte Salt + byte[] salt = { + (byte)0xE3, (byte)0x03, (byte)0x9B, (byte)0xA9, + (byte)0xC8, (byte)0x16, (byte)0x35, (byte)0x56 + }; + // Iteration count + int iterationCount = 19; + Cipher dcipher; + + // Create the key + KeySpec keySpec = new PBEKeySpec( + key.trim().toCharArray(), salt, iterationCount); + SecretKey secretKey = SecretKeyFactory.getInstance( + "PBEWithMD5AndDES").generateSecret(keySpec); + dcipher = Cipher.getInstance(secretKey.getAlgorithm()); + + // Prepare the parameter to the ciphers + AlgorithmParameterSpec paramSpec = + new PBEParameterSpec(salt, iterationCount); + + // Create the ciphers + dcipher.init(Cipher.DECRYPT_MODE, secretKey, paramSpec); + + byte[] dec = DatatypeConverter.parseBase64Binary(encryptedText.trim()); + return new String(dcipher.doFinal(dec), StandardCharsets.UTF_8); + } } diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionXMLUtil.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionXMLUtil.java new file mode 100644 index 00000000..fedf8470 --- /dev/null +++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/encrypt/EncryptionXMLUtil.java @@ -0,0 +1,149 @@ +/* Copyright 2018 Norconex Inc. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.norconex.commons.lang.encrypt; + +import java.io.IOException; +import java.io.Reader; +import java.io.Writer; + +import javax.xml.stream.XMLStreamException; +import javax.xml.stream.XMLStreamWriter; + +import org.apache.commons.configuration.XMLConfiguration; +import org.apache.commons.lang3.StringUtils; + +import com.norconex.commons.lang.config.IXMLConfigurable; +import com.norconex.commons.lang.config.XMLConfigurationUtil; +import com.norconex.commons.lang.xml.EnhancedXMLStreamWriter; + +/** + *

+ * Utility methods for loading and saving {@link EncryptionKey} with + * {@link IXMLConfigurable} objects or other XML-driven classes. + *

+ * + * @author Pascal Essiembre + * @since 1.15.0 + */ +public class EncryptionXMLUtil { + + private EncryptionXMLUtil() { + super(); + } + + /** + * Convenience method for loading an encryption key from an XML reader. + * @param in an XML reader + * @param tagPrefix prefix of the XML tag names being loaded. + * @param defaultKey default encryption key + * @return encryption key + * @see IXMLConfigurable + */ + public static EncryptionKey loadFromXML( + Reader in, String tagPrefix, EncryptionKey defaultKey) { + XMLConfiguration xml = XMLConfigurationUtil.newXMLConfiguration(in); + return loadFromXML(xml, tagPrefix, defaultKey); + } + /** + * Convenience method for loading an encryption key from an + * {@link XMLConfiguration}. + * @param xml xml configuration + * @param tagPrefix prefix of the XML tag names being loaded. + * @param defaultKey default encryption key + * @return encryption key + * @see IXMLConfigurable + */ + public static EncryptionKey loadFromXML( + XMLConfiguration xml, String tagPrefix, EncryptionKey defaultKey) { + String tagKey = StringUtils.trimToEmpty(tagPrefix); + tagKey = tagKey.length() > 0 ? tagKey + "Key" : "key"; + String tagSource = tagKey + "Source"; + String tagSize = tagKey + "Size"; + + String xmlKey = xml.getString(tagKey, null); + if (StringUtils.isNotBlank(xmlKey)) { + String xmlSource = xml.getString(tagSource, null); + Integer size = xml.getInteger( + tagSize, EncryptionKey.DEFAULT_KEY_SIZE); + EncryptionKey.Source source = null; + if (StringUtils.isNotBlank(xmlSource)) { + source = EncryptionKey.Source.valueOf(xmlSource.toUpperCase()); + } + return new EncryptionKey(xmlKey, source, size); + } + return defaultKey; + } + + /** + * Convenience method for saving an encryption key to an XML writer. + * @param writer a writer + * @param tagPrefix Prefix of the XML tag names being saved. If + * null, no prefix is used (not recommended unless + * wrapped in a parent tag). + * @param encryptionKey the encryption key to save + * @throws IOException problem saving to XML + * @see IXMLConfigurable + */ + public static void saveToXML( + Writer writer, String tagPrefix, EncryptionKey encryptionKey) + throws IOException { + try { + saveToXML(new EnhancedXMLStreamWriter(writer), + tagPrefix, encryptionKey); + } catch (XMLStreamException e) { + throw new IOException("Cannot save as XML.", e); + + } + } + /** + * Convenience method for saving an encryption key to an + * {@link XMLStreamWriter}. + * @param writer an XML writer + * @param tagPrefix Prefix of the XML tag names being saved. If + * null, no prefix is used (not recommended unless + * wrapped in a parent tag). + * @param encryptionKey the encryption key to save + * @throws IOException problem saving to XML + * @see IXMLConfigurable + */ + public static void saveToXML(XMLStreamWriter writer, + String tagPrefix, EncryptionKey encryptionKey) throws IOException { + + String tagKey = StringUtils.trimToEmpty(tagPrefix); + tagKey = tagKey.length() > 0 ? tagKey + "Key" : "key"; + String tagSource = tagKey + "Source"; + String tagSize = tagKey + "Size"; + + try { + EnhancedXMLStreamWriter w = null; + if (writer instanceof EnhancedXMLStreamWriter) { + w = (EnhancedXMLStreamWriter) writer; + } else { + w = new EnhancedXMLStreamWriter(writer); + } + + if (encryptionKey != null) { + w.writeElementString(tagKey, encryptionKey.getValue()); + w.writeElementInteger(tagSize, encryptionKey.getSize()); + if (encryptionKey.getSource() != null) { + w.writeElementString(tagSource, + encryptionKey.getSource().name().toLowerCase()); + } + } + } catch (XMLStreamException e) { + throw new IOException("Cannot save as XML.", e); + } + } +} diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/net/ProxySettings.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/net/ProxySettings.java index b198fc1d..1dac78e0 100644 --- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/net/ProxySettings.java +++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/net/ProxySettings.java @@ -1,4 +1,4 @@ -/* Copyright 2017 Norconex Inc. +/* Copyright 2018 Norconex Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -39,11 +39,12 @@ import com.norconex.commons.lang.config.XMLConfigurationUtil; import com.norconex.commons.lang.encrypt.EncryptionKey; import com.norconex.commons.lang.encrypt.EncryptionUtil; +import com.norconex.commons.lang.encrypt.EncryptionXMLUtil; import com.norconex.commons.lang.xml.EnhancedXMLStreamWriter; /** * Convenience class for implementation requiring proxy settings. - * + * * @author Pascal Essiembre * @since 1.14.0 */ @@ -58,7 +59,7 @@ public class ProxySettings implements IXMLConfigurable, Serializable { private String proxyPassword; private EncryptionKey proxyPasswordKey; private String proxyRealm; - + public ProxySettings() { super(); } @@ -120,7 +121,7 @@ public ProxySettings setProxyRealm(String proxyRealm) { public boolean isSet() { return StringUtils.isNotBlank(proxyHost); } - + public void copyFrom(ProxySettings another) { proxyHost = another.proxyHost; proxyPort = another.proxyPort; @@ -130,7 +131,7 @@ public void copyFrom(ProxySettings another) { proxyPasswordKey = another.proxyPasswordKey; proxyRealm = another.proxyRealm; } - + /** * Creates an Apache {@link HttpHost}. * @return HttpHost or null if proxy is not set. @@ -149,7 +150,7 @@ public AuthScope createAuthScope() { } public Credentials createCredentials() { if (isSet() && StringUtils.isNotBlank(proxyUsername)) { - String password = + String password = EncryptionUtil.decrypt(proxyPassword, proxyPasswordKey); return new UsernamePasswordCredentials(proxyUsername, password); } @@ -170,7 +171,7 @@ public CredentialsProvider createCredentialsProvider() { protected String getXmlTag() { return "proxy"; } - + /** * Loads from a {@link #getXmlTag()} tag. * @param in XML reader @@ -189,8 +190,8 @@ public void loadProxyFromXML(XMLConfiguration xml) { proxyScheme = xml.getString("proxyScheme", proxyScheme); proxyUsername = xml.getString("proxyUsername", proxyUsername); proxyPassword = xml.getString("proxyPassword", proxyPassword); - proxyPasswordKey = - loadXMLPasswordKey(xml, "proxyPasswordKey", proxyPasswordKey); + proxyPasswordKey = EncryptionXMLUtil.loadFromXML( + xml, "proxyPassword", proxyPasswordKey); proxyRealm = xml.getString("proxyRealm", proxyRealm); } /** @@ -214,49 +215,29 @@ public void saveToXML(Writer out) throws IOException { /** * Saves assuming we are already in a parent tag. * @param out XML stream writer + * @throws IOException problem saving stream to XML */ - public void saveProxyToXML(XMLStreamWriter out) throws XMLStreamException { + public void saveProxyToXML(XMLStreamWriter out) throws IOException { EnhancedXMLStreamWriter writer; if (out instanceof EnhancedXMLStreamWriter) { writer = (EnhancedXMLStreamWriter) out; } else { writer = new EnhancedXMLStreamWriter(out); } - writer.writeElementString("proxyHost", proxyHost); - writer.writeElementInteger("proxyPort", proxyPort); - writer.writeElementString("proxyScheme", proxyScheme); - writer.writeElementString("proxyUsername", proxyUsername); - writer.writeElementString("proxyPassword", proxyPassword); - saveXMLPasswordKey(writer, "proxyPasswordKey", proxyPasswordKey); - writer.writeElementString("proxyRealm", proxyRealm); - } - - private void saveXMLPasswordKey(EnhancedXMLStreamWriter writer, - String field, EncryptionKey key) throws XMLStreamException { - if (key == null) { - return; - } - writer.writeElementString(field, key.getValue()); - if (key.getSource() != null) { - writer.writeElementString( - field + "Source", key.getSource().name().toLowerCase()); - } - } - - private EncryptionKey loadXMLPasswordKey( - XMLConfiguration xml, String field, EncryptionKey defaultKey) { - String xmlKey = xml.getString(field, null); - String xmlSource = xml.getString(field + "Source", null); - if (StringUtils.isBlank(xmlKey)) { - return defaultKey; - } - EncryptionKey.Source source = null; - if (StringUtils.isNotBlank(xmlSource)) { - source = EncryptionKey.Source.valueOf(xmlSource.toUpperCase()); + try { + writer.writeElementString("proxyHost", proxyHost); + writer.writeElementInteger("proxyPort", proxyPort); + writer.writeElementString("proxyScheme", proxyScheme); + writer.writeElementString("proxyUsername", proxyUsername); + writer.writeElementString("proxyPassword", proxyPassword); + EncryptionXMLUtil.saveToXML( + writer, "proxyPassword", proxyPasswordKey); + writer.writeElementString("proxyRealm", proxyRealm); + } catch (XMLStreamException e) { + throw new IOException("Cannot save as XML.", e); } - return new EncryptionKey(xmlKey, source); } - + @Override public boolean equals(final Object other) { if (!(other instanceof ProxySettings)) { @@ -298,5 +279,5 @@ public String toString() { .append("proxyPasswordKey", proxyPasswordKey) .append("proxyRealm", proxyRealm) .toString(); - } + } } diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/url/HttpURL.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/url/HttpURL.java index b0e2b451..59578864 100644 --- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/url/HttpURL.java +++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/url/HttpURL.java @@ -1,4 +1,4 @@ -/* Copyright 2010-2017 Norconex Inc. +/* Copyright 2010-2018 Norconex Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -35,7 +35,7 @@ * * @author Pascal Essiembre */ -@SuppressWarnings("nls") +//TODO rename MutableURL public class HttpURL implements Serializable { private static final long serialVersionUID = -8886393027925815099L; @@ -52,7 +52,7 @@ public class HttpURL implements Serializable { private QueryString queryString; private String host; - private int port = DEFAULT_HTTP_PORT; + private int port = -1; private String path; private String protocol; private final String encoding; @@ -103,31 +103,38 @@ public HttpURL(String url, String encoding) { } else { this.encoding = encoding; } - if (url.matches("\\w+://.+")) { + + String u = StringUtils.trimToEmpty(url); + if (u.matches("[a-zA-Z][a-zA-Z0-9\\+\\-\\.]*:.*")) { URL urlwrap; try { - urlwrap = new URL(url); + urlwrap = new URL(u); } catch (MalformedURLException e) { - throw new URLException("Could not interpret URL: " + url, e); + throw new URLException("Could not interpret URL: " + u, e); } - protocol = StringUtils.substringBefore(url, ":"); + protocol = StringUtils.substringBefore(u, ":"); host = urlwrap.getHost(); port = urlwrap.getPort(); if (port < 0) { - if (StringUtils.startsWithIgnoreCase(url, PROTOCOL_HTTPS)) { + if (StringUtils.startsWithIgnoreCase(u, PROTOCOL_HTTPS)) { port = DEFAULT_HTTPS_PORT; } else if ( - StringUtils.startsWithIgnoreCase(url, PROTOCOL_HTTP)) { + StringUtils.startsWithIgnoreCase(u, PROTOCOL_HTTP)) { port = DEFAULT_HTTP_PORT; } } path = urlwrap.getPath(); fragment = urlwrap.getRef(); + } else { + path = u.replaceFirst("^(.*?)([\\?\\#])(.*)", "$1"); + if (StringUtils.contains(u, "#")) { + fragment = u.replaceFirst("^(.*?)(\\#)(.*)", "$3"); + } } // Parameters - if (StringUtils.contains(url, "?")) { - queryString = new QueryString(url, encoding); + if (StringUtils.contains(u, "?")) { + queryString = new QueryString(u, encoding); } } @@ -353,16 +360,20 @@ public static String getRoot(String url) { @Override public String toString() { StringBuilder b = new StringBuilder(); - b.append(protocol); - b.append("://"); - b.append(host); - + if (StringUtils.isNotBlank(protocol)) { + b.append(protocol); + b.append("://"); + } + if (StringUtils.isNotBlank(host)) { + b.append(host); + } if (!isPortDefault() && port != -1) { b.append(':'); b.append(port); } if (StringUtils.isNotBlank(path)) { - if (!path.startsWith("/")) { + // If no scheme/host/port, leave the path as is + if (b.length() > 0 && !path.startsWith("/")) { b.append('/'); } b.append(encodePath(path)); @@ -374,6 +385,7 @@ public String toString() { b.append("#"); b.append(encodePath(fragment)); } + return b.toString(); } diff --git a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/url/QueryString.java b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/url/QueryString.java index 4571ca62..53b5ec10 100644 --- a/norconex-commons-lang/src/main/java/com/norconex/commons/lang/url/QueryString.java +++ b/norconex-commons-lang/src/main/java/com/norconex/commons/lang/url/QueryString.java @@ -94,6 +94,7 @@ public QueryString(String urlWithQueryString, String encoding) { } String paramString = urlWithQueryString; if (StringUtils.contains(paramString, "?")) { + paramString = StringUtils.substringBefore(paramString, "#"); paramString = paramString.replaceAll("(.*?)(\\?)(.*)", "$3"); } String[] paramParts = paramString.split("\\&"); diff --git a/norconex-commons-lang/src/site/markdown/download.md.vm b/norconex-commons-lang/src/site/markdown/download.md.vm index b53c2f5c..59434825 100644 --- a/norconex-commons-lang/src/site/markdown/download.md.vm +++ b/norconex-commons-lang/src/site/markdown/download.md.vm @@ -30,6 +30,12 @@ $h2 Binaries **Older Releases** + * [1.14.0]($nexusPath/1.14.0/norconex-commons-lang-1.14.0.zip) + [[Release Notes](changes-report.html#a1.14.0)] + * [1.13.1]($nexusPath/1.13.1/norconex-commons-lang-1.13.1.zip) + [[Release Notes](changes-report.html#a1.13.1)] + * [1.13.0]($nexusPath/1.13.0/norconex-commons-lang-1.13.0.zip) + [[Release Notes](changes-report.html#a1.13.0)] * [1.12.3]($nexusPath/1.12.2/norconex-commons-lang-1.12.3.zip) [[Release Notes](changes-report.html#a1.12.3)] * [1.12.2]($nexusPath/1.12.2/norconex-commons-lang-1.12.2.zip) diff --git a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/encrypt/EncryptionUtilTest.java b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/encrypt/EncryptionUtilTest.java index 8d092cdb..5cad3001 100644 --- a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/encrypt/EncryptionUtilTest.java +++ b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/encrypt/EncryptionUtilTest.java @@ -1,4 +1,4 @@ -/* Copyright 2015 Norconex Inc. +/* Copyright 20185 Norconex Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,9 +14,15 @@ */ package com.norconex.commons.lang.encrypt; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; + import org.junit.Assert; +import org.junit.Assume; import org.junit.Test; + public class EncryptionUtilTest { @Test @@ -27,4 +33,38 @@ public void testEncrypt() { String decryptedText = EncryptionUtil.decrypt(encryptedText, key); Assert.assertEquals(text, decryptedText); } + + @Test + public void testEncryptTwice() { + EncryptionKey key = new EncryptionKey("this is my secret key."); + String text = "please encrypt this text."; + String encryptedText1 = EncryptionUtil.encrypt(text, key); + String encryptedText2 = EncryptionUtil.encrypt(text, key); + Assert.assertNotEquals(encryptedText1, encryptedText2); + } + + @Test + public void testDecryptLegacy() { + EncryptionKey key = new EncryptionKey("This is an encryption key"); + String expectedClearText = "Please encrypt this text"; + String encryptedText = "aeEFKa0uXMUHT4UyeFtuHjm37NQw3vEaxY03EkkD2qM="; + String actualClearText = EncryptionUtil.decrypt(encryptedText, key); + Assert.assertEquals(expectedClearText, actualClearText); + } + + @Test + public void testAes256bitEncryptionKey() throws NoSuchAlgorithmException { + + // NOTE: this test should be true on Java 8 u162+ or on Java 9, or on + // any Java where JCE Unlimited Strength has been applied + Assume.assumeTrue(Cipher.getMaxAllowedKeyLength("AES") >= 256); + + // Create round-trip encryption key + EncryptionKey key = new EncryptionKey("This as an encryption key", 256); + String text = "please encrypt this text"; + String encryptedText = EncryptionUtil.encrypt(text, key); + String decryptedText = EncryptionUtil.decrypt(encryptedText, key); + + Assert.assertEquals(text, decryptedText); + } } diff --git a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/net/ProxySettingsTest.java b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/net/ProxySettingsTest.java index 9a272dd2..19c07921 100644 --- a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/net/ProxySettingsTest.java +++ b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/net/ProxySettingsTest.java @@ -1,4 +1,4 @@ -/* Copyright 2017 Norconex Inc. +/* Copyright 2017-2018 Norconex Inc. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -29,7 +29,7 @@ public void testWriteRead() throws IOException { ProxySettings ps = new ProxySettings(); ps.setProxyHost("myhost"); ps.setProxyPassword("mypassword"); - ps.setProxyPasswordKey(new EncryptionKey("keyvalue", Source.KEY)); + ps.setProxyPasswordKey(new EncryptionKey("keyvalue", Source.KEY, 256)); ps.setProxyPort(99); ps.setProxyRealm("realm"); ps.setProxyScheme("sheme"); diff --git a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/HttpURLTest.java b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/HttpURLTest.java index a9c0e852..6de133e2 100644 --- a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/HttpURLTest.java +++ b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/HttpURLTest.java @@ -146,4 +146,61 @@ public void testInvalidURL() { t = "http://www.example.com/%22path%22"; assertEquals(t, new HttpURL(s).toString()); } + + @Test + public void testURLWithLeadingTrailingSpaces() { + s = " http://www.example.com/path "; + t = "http://www.example.com/path"; + assertEquals(t, new HttpURL(s).toString()); + } + + @Test + public void testNullOrBlankURLs() { + s = null; + t = ""; + assertEquals(t, new HttpURL(s).toString()); + s = ""; + t = ""; + assertEquals(t, new HttpURL(s).toString()); + s = " "; + t = ""; + assertEquals(t, new HttpURL(s).toString()); + } + + @Test + public void testRelativeURLs() { + s = "./blah"; + t = "./blah"; + assertEquals(t, new HttpURL(s).toString()); + s = "/blah"; + t = "/blah"; + assertEquals(t, new HttpURL(s).toString()); + s = "blah?param=value#frag"; + t = "blah?param=value#frag"; + assertEquals(t, new HttpURL(s).toString()); + } + + @Test + public void testFileProtocol() { + // Encode non-URI characters + s = "file:///etc/some dir/my file.txt"; + t = "file:///etc/some%20dir/my%20file.txt"; + assertEquals(t, new HttpURL(s).toString()); + + s = "file://./dir/another-dir/path"; + t = "file://./dir/another-dir/path"; + assertEquals(t, new HttpURL(s).toString()); + + s = "file://localhost/c:/WINDOWS/éà.txt"; + t = "file://localhost/c:/WINDOWS/%C3%A9%C3%A0.txt"; + assertEquals(t, new HttpURL(s).toString()); + + s = "file:///c:/WINDOWS/file.txt"; + t = "file:///c:/WINDOWS/file.txt"; + assertEquals(t, new HttpURL(s).toString()); + + s = "file:/c:/WINDOWS/file.txt"; + t = "file:///c:/WINDOWS/file.txt"; + assertEquals(t, new HttpURL(s).toString()); + } } \ No newline at end of file diff --git a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/URLNormalizerTest.java b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/URLNormalizerTest.java index 27d9f0f2..c5b0a687 100644 --- a/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/URLNormalizerTest.java +++ b/norconex-commons-lang/src/test/java/com/norconex/commons/lang/url/URLNormalizerTest.java @@ -510,6 +510,29 @@ public void testRemoveSessionIds() { } // /myservlet;jsessionid=1E6FEC0D14D044541DD84D2D013D29ED?_option=XX[b]'[/b];[ // PHPSESSID=f9f2770d591366bc + + // Test for supporting file:// scheme, from here: + // https://github.com/Norconex/commons-lang/issues/11 + @Test + public void testFileScheme() { + + // Encode non-URI characters + s = "file:///etc/some dir/my file.txt"; + t = "file:///etc/some%20dir/my%20file.txt"; + assertEquals(t, n(s).encodeNonURICharacters().toString()); + + s = "file://./dir/another-dir/path"; + t = "file://./dir/another-dir/path"; + assertEquals(t, n(s).encodeNonURICharacters().toString()); + + s = "file://localhost/c:/WINDOWS/éà.txt"; + t = "file://localhost/c:/WINDOWS/%C3%A9%C3%A0.txt"; + assertEquals(t, n(s).encodeNonURICharacters().toString()); + + s = "file:///c:/WINDOWS/file.txt"; + t = "file:///c:/WINDOWS/file.txt"; + assertEquals(t, n(s).encodeNonURICharacters().toString()); + } private URLNormalizer n(String url) { return new URLNormalizer(url);