Updated client registration (store client secret hashcode).
Change-Id: Ifa14f1439b617be2b1c37c31a8c40298efcc6314
diff --git a/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java b/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
index f5f04ad..61b158e 100644
--- a/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
+++ b/full/src/main/java/de/ids_mannheim/korap/config/FullConfiguration.java
@@ -6,6 +6,8 @@
import java.util.Properties;
import java.util.regex.Pattern;
+import de.ids_mannheim.korap.interfaces.EncryptionIface;
+
/** Configuration for Kustvakt full version including properties concerning
* authentication and licenses.
*
@@ -42,6 +44,8 @@
private boolean isSoftDeleteGroup;
private boolean isSoftDeleteGroupMember;
+ private EncryptionIface.Encryption encryption;
+
public FullConfiguration (Properties properties) throws IOException {
super(properties);
}
@@ -59,6 +63,8 @@
setMailConfiguration(properties);
ldapConfig = properties.getProperty("ldap.config");
+ setEncryption(Enum.valueOf(EncryptionIface.Encryption.class,
+ properties.getProperty("security.encryption", "BCRYPT")));
}
private void setMailConfiguration (Properties properties) {
@@ -283,4 +289,12 @@
this.emailAddressRetrieval = emailAddressRetrieval;
}
+ public EncryptionIface.Encryption getEncryption () {
+ return encryption;
+ }
+
+ public void setEncryption (EncryptionIface.Encryption encryption) {
+ this.encryption = encryption;
+ }
+
}
diff --git a/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java b/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java
index 21b3e2a..5ab083f 100644
--- a/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java
+++ b/full/src/main/java/de/ids_mannheim/korap/dao/OAuth2ClientDao.java
@@ -26,7 +26,7 @@
@PersistenceContext
private EntityManager entityManager;
- public void registerClient (String id, String secret, String name,
+ public void registerClient (String id, String secretHashcode, String name,
OAuth2ClientType type, String url, int urlHashCode,
String redirectURI, String registeredBy) throws KustvaktException {
ParameterChecker.checkStringValue(id, "client id");
@@ -39,7 +39,7 @@
OAuth2Client client = new OAuth2Client();
client.setId(id);
client.setName(name);
- client.setSecret(secret);
+ client.setSecret(secretHashcode);
client.setType(type);
client.setUrl(url);
client.setUrlHashCode(urlHashCode);
diff --git a/full/src/main/java/de/ids_mannheim/korap/encryption/DefaultEncryption.java b/full/src/main/java/de/ids_mannheim/korap/encryption/DefaultEncryption.java
new file mode 100644
index 0000000..0f18f20
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/encryption/DefaultEncryption.java
@@ -0,0 +1,80 @@
+package de.ids_mannheim.korap.encryption;
+
+import de.ids_mannheim.korap.config.ContextHolder;
+import de.ids_mannheim.korap.config.Configurable;
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.interfaces.EncryptionIface;
+import de.ids_mannheim.korap.user.User;
+
+import java.io.UnsupportedEncodingException;
+import java.math.BigInteger;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Map;
+
+/**
+ * @author hanl
+ * @date 05/06/2015
+ */
+@Configurable(ContextHolder.KUSTVAKT_ENCRYPTION)
+public class DefaultEncryption implements EncryptionIface {
+
+ private SecureRandom randomizer;
+
+
+ public DefaultEncryption () {
+ randomizer = new SecureRandom();
+ }
+
+
+ @Override
+ public String secureHash (String input, String salt)
+ throws KustvaktException {
+ return null;
+ }
+
+
+ @Override
+ public String secureHash (String input) throws NoSuchAlgorithmException,
+ UnsupportedEncodingException, KustvaktException {
+ return null;
+ }
+
+
+ @Override
+ public boolean checkHash (String plain, String hash, String salt) {
+ return false;
+ }
+
+
+ @Override
+ public boolean checkHash (String plain, String hash) {
+ return false;
+ }
+
+
+ @Override
+ public String createToken (boolean hash, Object ... obj) {
+ return createToken();
+
+ }
+
+
+ @Override
+ public String createToken () {
+ return new BigInteger(100, randomizer).toString(20);
+ }
+
+
+ @Override
+ public String createRandomNumber (Object ... obj) {
+ return createToken();
+ }
+
+
+ @Override
+ public String encodeBase () {
+ return null;
+ }
+
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/encryption/KustvaktEncryption.java b/full/src/main/java/de/ids_mannheim/korap/encryption/KustvaktEncryption.java
new file mode 100644
index 0000000..b73a524
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/encryption/KustvaktEncryption.java
@@ -0,0 +1,299 @@
+package de.ids_mannheim.korap.encryption;
+
+import java.io.UnsupportedEncodingException;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+
+import org.apache.commons.codec.EncoderException;
+import org.apache.commons.codec.binary.Base64;
+import org.apache.commons.lang.RandomStringUtils;
+import org.mindrot.jbcrypt.BCrypt;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import de.ids_mannheim.korap.config.FullConfiguration;
+import de.ids_mannheim.korap.interfaces.EncryptionIface;
+
+public class KustvaktEncryption implements EncryptionIface {
+
+ private static final String ALGORITHM = "SHA-256";
+ private static Logger jlog = LoggerFactory
+ .getLogger(KustvaktEncryption.class);
+
+ private final FullConfiguration config;
+
+
+ public KustvaktEncryption (FullConfiguration config) {
+ jlog.info("initializing KorAPEncryption implementation");
+ this.config = config;
+ }
+
+
+ public static boolean matchTokenByteCode (Object param) {
+ if (!(param instanceof String))
+ return false;
+ String token = (String) param;
+ byte[] bytes = token.getBytes();
+ return 64 == bytes.length;
+ }
+
+
+ private String encodeBase (byte[] bytes) throws EncoderException {
+ return Base64.encodeBase64String(bytes);
+ }
+
+
+ @Override
+ public String encodeBase () {
+ try {
+ return encodeBase(this.createSecureRandom(24));
+ }
+ catch (EncoderException e) {
+ return "";
+ }
+ }
+
+
+ public String secureHash (String input) {
+ return secureHash(input, "");
+ }
+
+
+ @Override
+ public String secureHash (String input, String salt) {
+ String hashString = "";
+ switch (config.getEncryption()) {
+ case ESAPICYPHER:
+ break;
+ case SIMPLE:
+ try {
+ MessageDigest md = MessageDigest.getInstance("SHA-256");
+ md.update(input.getBytes("UTF-8"));
+ byte[] digest = md.digest();
+
+ for (byte b : digest)
+ hashString += String.format("%02x", b);
+ }
+ catch (UnsupportedEncodingException | NoSuchAlgorithmException e) {
+ e.printStackTrace();
+ }
+ break;
+ case BCRYPT:
+ hashString = bcryptHash(input, salt);
+ break;
+ default:
+ jlog.warn("Invalid value: {}", config.getEncryption());
+ break;
+ }
+ return hashString;
+ }
+
+
+
+ public String hash (String input) {
+ String hashString = "";
+ MessageDigest md;
+ try {
+ md = MessageDigest.getInstance(ALGORITHM);
+ md.update(input.getBytes("UTF-8"));
+ }
+ catch (NoSuchAlgorithmException e) {
+ return "";
+ }
+ catch (UnsupportedEncodingException e) {
+ return "";
+ }
+
+ byte[] digest = md.digest();
+
+ for (byte b : digest) {
+ hashString += String.format("%02x", b);
+ }
+ return hashString;
+ }
+
+
+ /**
+ * // some sort of algorithm to create token and isSystem
+ * regularly the integrity
+ * // of the token
+ * public String createAuthToken() {
+ * final byte[] rNumber = SecureRGenerator
+ * .getNextSecureRandom(SecureRGenerator.TOKEN_RANDOM_SIZE);
+ * String hash;
+ * try {
+ * hash = produceSimpleHash(SecureRGenerator.toHex(rNumber));
+ * } catch (NoSuchAlgorithmException |
+ * UnsupportedEncodingException e) {
+ * return "";
+ * }
+ * return hash;
+ * }
+ */
+
+ private byte[] createSecureRandom (int size) {
+ return SecureRGenerator.getNextSecureRandom(size);
+ }
+
+
+ /**
+ * does this need to be equal for every iteration?!
+ * @param hash
+ * @param obj
+ * @return
+ */
+ @Override
+ public String createToken (boolean hash, Object ... obj) {
+ StringBuffer b = new StringBuffer();
+ try {
+ for (Object o : obj) {
+ b.append(" | ");
+ b.append(o);
+ }
+ if (hash)
+ return encodeBase(hash(b.toString().trim()).getBytes());
+ else
+ return encodeBase(b.toString().trim().getBytes());
+ }
+ catch (EncoderException e) {
+ return "";
+ }
+
+ }
+
+
+ @Override
+ public String createToken () {
+ return RandomStringUtils.randomAlphanumeric(64);
+
+ // EM: code from MH
+// String encoded;
+// String v = RandomStringUtils.randomAlphanumeric(64);
+// encoded = hash(v);
+// jlog.trace("creating new token {}", encoded);
+// return encoded;
+ }
+
+
+ @Override
+ public String createRandomNumber (Object ... obj) {
+ final byte[] rNumber = SecureRGenerator
+ .getNextSecureRandom(SecureRGenerator.CORPUS_RANDOM_SIZE);
+ if (obj.length == 0) {
+ obj = new Object[1];
+ obj[0] = rNumber;
+ }
+ return createToken(false, obj);
+ }
+
+
+ @Override
+ public boolean checkHash (String plain, String hash, String salt) {
+ String pw = "";
+ switch (config.getEncryption()) {
+ case ESAPICYPHER:
+ pw = secureHash(plain, salt);
+ break;
+ case BCRYPT:
+ try {
+ return BCrypt.checkpw(plain, hash);
+ }
+ catch (IllegalArgumentException e) {
+ return false;
+ }
+ case SIMPLE:
+ pw = hash(plain);
+ break;
+ }
+ return pw.equals(hash);
+ }
+
+
+ @Override
+ public boolean checkHash (String plain, String hash) {
+ switch (config.getEncryption()) {
+ case ESAPICYPHER:
+ return secureHash(plain).equals(hash);
+ case BCRYPT:
+ try {
+ return BCrypt.checkpw(plain, hash);
+ }
+ catch (IllegalArgumentException e) {
+ return false;
+ }
+ case SIMPLE:
+ return hash(plain).equals(hash);
+ }
+ return false;
+ }
+
+
+ private String bcryptHash (String text, String salt) {
+ if (salt == null || salt.isEmpty())
+ salt = BCrypt.gensalt(config.getLoadFactor());
+ return BCrypt.hashpw(text, salt);
+ }
+
+
+ @Override
+ public String toString () {
+ return this.getClass().getCanonicalName();
+ }
+
+ public static class SecureRGenerator {
+ private static final String SHA1_PRNG = "SHA1PRNG";
+ protected static final int DEFAULT_RANDOM_SIZE = 128;
+ protected static final int TOKEN_RANDOM_SIZE = 128;
+ protected static final int USERID_RANDOM_SIZE = 64;
+ protected static final int CORPUS_RANDOM_SIZE = 48;
+ private static final char[] HEX_DIGIT = { '0', '1', '2', '3', '4', '5',
+ '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f', 'z', 'x',
+ 'h', 'q', 'w' };
+ private static final SecureRandom sRandom__;
+
+ static {
+ try {
+ sRandom__ = SecureRandom.getInstance("SHA1PRNG");
+ }
+ catch (NoSuchAlgorithmException e) {
+ throw new Error(e);
+ }
+ }
+
+
+ public static byte[] getNextSecureRandom (int bits) {
+ if (bits % 8 != 0) {
+ throw new IllegalArgumentException(
+ "Size is not divisible by 8!");
+ }
+
+ byte[] bytes = new byte[bits / 8];
+
+ sRandom__.nextBytes(bytes);
+
+ return bytes;
+ }
+
+
+ public static String toHex (byte[] bytes) {
+ if (bytes == null) {
+ return null;
+ }
+
+ StringBuilder buffer = new StringBuilder(bytes.length * 2);
+ for (byte thisByte : bytes) {
+ buffer.append(byteToHex(thisByte));
+ }
+
+ return buffer.toString();
+ }
+
+
+ private static String byteToHex (byte b) {
+ char[] array = { HEX_DIGIT[(b >> 4 & 0xF)], HEX_DIGIT[(b & 0xF)] };
+ return new String(array);
+ }
+ }
+
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java b/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java
index c6d179f..35b6dce 100644
--- a/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java
+++ b/full/src/main/java/de/ids_mannheim/korap/entity/OAuth2Client.java
@@ -24,6 +24,7 @@
@Id
private String id;
private String name;
+ // Secret hashcode is stored instead of plain secret
private String secret;
@Enumerated(EnumType.STRING)
private OAuth2ClientType type;
diff --git a/full/src/main/java/de/ids_mannheim/korap/interfaces/EncryptionIface.java b/full/src/main/java/de/ids_mannheim/korap/interfaces/EncryptionIface.java
new file mode 100644
index 0000000..134ebdb
--- /dev/null
+++ b/full/src/main/java/de/ids_mannheim/korap/interfaces/EncryptionIface.java
@@ -0,0 +1,76 @@
+package de.ids_mannheim.korap.interfaces;
+
+import de.ids_mannheim.korap.exceptions.KustvaktException;
+import de.ids_mannheim.korap.user.User;
+
+import java.io.UnsupportedEncodingException;
+import java.security.NoSuchAlgorithmException;
+import java.util.Map;
+
+public interface EncryptionIface {
+
+ public enum Encryption {
+ @Deprecated
+ SIMPLE, ESAPICYPHER, BCRYPT
+ }
+
+
+ /**
+ * One-way hashing of String input. Used to canonicalize
+ *
+ * @param input
+ * @param salt
+ * @return
+ */
+ public String secureHash (String input, String salt)
+ throws KustvaktException;
+
+
+ public String secureHash (String input) throws NoSuchAlgorithmException,
+ UnsupportedEncodingException, KustvaktException;
+
+
+ /**
+ * @param plain
+ * @param hash
+ * @param salt
+ * @return
+ */
+ public boolean checkHash (String plain, String hash, String salt);
+
+
+ public boolean checkHash (String plain, String hash);
+
+
+ /**
+ * create random String to be used as authentication token
+ *
+ * @return
+ */
+ public String createToken (boolean hash, Object ... obj);
+
+
+ public String createToken ();
+
+
+ /**
+ * create a random Integer to be used as ID for databases
+ *
+ * @return
+ */
+ public String createRandomNumber (Object ... obj);
+
+
+ public String encodeBase ();
+
+
+ // @Deprecated
+ //public Map<String, Object> validateMap (Map<String, Object> map)
+ // throws KustvaktException;
+
+
+ //@Deprecated
+ //public String validateEntry (String input, String type)
+ // throws KustvaktException;
+
+}
diff --git a/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java b/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java
index c6c732b..fe27503 100644
--- a/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java
+++ b/full/src/main/java/de/ids_mannheim/korap/service/OAuth2ClientService.java
@@ -9,6 +9,7 @@
import de.ids_mannheim.korap.authentication.http.AuthorizationData;
import de.ids_mannheim.korap.authentication.http.HttpAuthorizationHandler;
+import de.ids_mannheim.korap.config.FullConfiguration;
import de.ids_mannheim.korap.constant.AuthenticationScheme;
import de.ids_mannheim.korap.constant.OAuth2ClientType;
import de.ids_mannheim.korap.dao.AdminDao;
@@ -34,6 +35,8 @@
private EncryptionIface encryption;
@Autowired
private HttpAuthorizationHandler authorizationHandler;
+ @Autowired
+ private FullConfiguration config;
public OAuth2ClientDto registerClient (OAuth2ClientJson clientJson,
@@ -49,6 +52,7 @@
}
String secret = null;
+ String secretHashcode = null;
if (clientJson.getType().equals(OAuth2ClientType.CONFIDENTIAL)) {
// RFC 6749:
// The authorization server MUST NOT issue client passwords or other
@@ -61,11 +65,13 @@
// specific device.
secret = encryption.createToken();
+ secretHashcode = encryption.secureHash(secret,
+ config.getPasscodeSaltField());
}
String id = encryption.createRandomNumber();
try {
- clientDao.registerClient(id, secret, clientJson.getName(),
+ clientDao.registerClient(id, secretHashcode, clientJson.getName(),
clientJson.getType(), clientJson.getUrl(),
clientJson.getUrl().hashCode(), clientJson.getRedirectURI(),
registeredBy);
@@ -162,7 +168,9 @@
.equals(AuthenticationScheme.BASIC)) {
authorizationHandler.parseBasicToken(authData);
if (!client.getId().equals(clientId)
- || !client.getSecret().equals(authData.getPassword())) {
+ || !encryption.checkHash(authData.getPassword(),
+ client.getSecret(),
+ config.getPasscodeSaltField())) {
throw new KustvaktException(
StatusCodes.AUTHENTICATION_FAILED,
"Client credentials are incorrect.");
diff --git a/full/src/main/resources/default-config.xml b/full/src/main/resources/default-config.xml
index b3bf566..76917d4 100644
--- a/full/src/main/resources/default-config.xml
+++ b/full/src/main/resources/default-config.xml
@@ -196,7 +196,7 @@
</bean>
<bean name="kustvakt_encryption"
- class="de.ids_mannheim.korap.interfaces.defaults.KustvaktEncryption">
+ class="de.ids_mannheim.korap.encryption.KustvaktEncryption">
<constructor-arg ref="kustvakt_config" />
</bean>