diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..70ed991 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +target +.DS_Store +.classpath +.project +.settings diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..10178f2 --- /dev/null +++ b/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2020 Matheus Salmi + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..532169e --- /dev/null +++ b/README.md @@ -0,0 +1,20 @@ +# keycloak-sha1 +Add SHA1 hashing support to Keycloak. + +## Requirements + +- Java 11 +- Maven 3.6 + +## Building + +1. `mvn install` +2. `mvn package` +3. It should generate a JAR archive under `./target/keycloak-sha1.jar` + +## Deploying to Keycloak + +1. Move the built JAR file to Keycloak's directory `standalone/deployments/` (on Keycloak under Docker: `/opt/jboss/keycloak/standalone/deployments`) +2. Watch the `standalone/deployments/` for the file `keycloak-sha1.jar.deployed` + +:warning: If you find instead the file `keycloak-sha1.jar.failed`, you can run the command `cat keycloak-sha1.jar.failed` to find out what went wrong with your deployment. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..83357ee --- /dev/null +++ b/pom.xml @@ -0,0 +1,88 @@ + + 4.0.0 + com.msalmi + keycloak-sha1 + + 0.0.1 + keycloak-sha1 + Adds SHA1 hashing support to Keycloak + + jar + + + 11 + 10.0.0 + 5.7.0 + + + + + + org.keycloak + keycloak-common + ${keycloak.version} + provided + + + org.keycloak + keycloak-core + ${keycloak.version} + provided + + + org.keycloak + keycloak-server-spi + ${keycloak.version} + provided + + + org.keycloak + keycloak-server-spi-private + ${keycloak.version} + provided + + + + + org.junit.jupiter + junit-jupiter-api + ${junit.version} + test + + + + + keycloak-sha1 + + + + org.apache.maven.plugins + maven-eclipse-plugin + 2.10 + + true + false + + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.8.1 + + ${java.release} + + + + + + org.apache.maven.plugins + maven-jar-plugin + 3.2.0 + + + + diff --git a/src/main/java/com/msalmi/SHA1HashProvider.java b/src/main/java/com/msalmi/SHA1HashProvider.java new file mode 100644 index 0000000..48b182f --- /dev/null +++ b/src/main/java/com/msalmi/SHA1HashProvider.java @@ -0,0 +1,58 @@ +package com.msalmi; + +import java.math.BigInteger; +import java.security.MessageDigest; + +import org.keycloak.credential.hash.PasswordHashProvider; +import org.keycloak.models.PasswordPolicy; +import org.keycloak.models.credential.PasswordCredentialModel; + +public class SHA1HashProvider implements PasswordHashProvider { + + private final String providerId; + + public SHA1HashProvider(String providerId) { + this.providerId = providerId; + } + + @Override + public void close() { + } + + @Override + public boolean policyCheck(PasswordPolicy policy, PasswordCredentialModel credential) { + return this.providerId.equals(credential.getPasswordCredentialData().getAlgorithm()); + } + + @Override + public PasswordCredentialModel encodedCredential(String rawPassword, int iterations) { + String encodedPassword = this.encode(rawPassword, iterations); + return PasswordCredentialModel.createFromValues(this.providerId, new byte[0], iterations, encodedPassword); + } + + @Override + public boolean verify(String rawPassword, PasswordCredentialModel credential) { + String encodedPassword = this.encode(rawPassword, credential.getPasswordCredentialData().getHashIterations()); + String hash = credential.getPasswordSecretData().getValue(); + return encodedPassword.equals(hash); + } + + @Override + public String encode(String rawPassword, int iterations) { + try { + MessageDigest md = MessageDigest.getInstance(this.providerId); + md.update(rawPassword.getBytes()); + + // convert the digest byte[] to BigInteger + var aux = new BigInteger(1, md.digest()); + + // convert BigInteger to 40-char lowercase string using leading 0s + return String.format("%040x", aux); + } catch (Exception e) { + // fail silently + } + + return null; + } + +} diff --git a/src/main/java/com/msalmi/SHA1HashProviderFactory.java b/src/main/java/com/msalmi/SHA1HashProviderFactory.java new file mode 100644 index 0000000..49e2fa5 --- /dev/null +++ b/src/main/java/com/msalmi/SHA1HashProviderFactory.java @@ -0,0 +1,33 @@ +package com.msalmi; + +import org.keycloak.Config.Scope; +import org.keycloak.credential.hash.PasswordHashProvider; +import org.keycloak.credential.hash.PasswordHashProviderFactory; +import org.keycloak.models.KeycloakSession; +import org.keycloak.models.KeycloakSessionFactory; + +public class SHA1HashProviderFactory implements PasswordHashProviderFactory { + public static final String ID = "SHA-1"; + + @Override + public PasswordHashProvider create(KeycloakSession session) { + return new SHA1HashProvider(getId()); + } + + @Override + public void init(Scope config) { + } + + @Override + public void postInit(KeycloakSessionFactory factory) { + } + + @Override + public void close() { + } + + @Override + public String getId() { + return ID; + } +} diff --git a/src/main/resources/META-INF/services/org.keycloak.credential.hash.PasswordHashProviderFactory b/src/main/resources/META-INF/services/org.keycloak.credential.hash.PasswordHashProviderFactory new file mode 100644 index 0000000..430640f --- /dev/null +++ b/src/main/resources/META-INF/services/org.keycloak.credential.hash.PasswordHashProviderFactory @@ -0,0 +1 @@ +com.msalmi.SHA1HashProviderFactory \ No newline at end of file diff --git a/src/test/java/com/msalmi/SHA1HashProviderTest.java b/src/test/java/com/msalmi/SHA1HashProviderTest.java new file mode 100644 index 0000000..3e85d62 --- /dev/null +++ b/src/test/java/com/msalmi/SHA1HashProviderTest.java @@ -0,0 +1,44 @@ +package com.msalmi; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import org.junit.jupiter.api.Test; + +public class SHA1HashProviderTest { + + @Test + public void encodeHelloWorld() { + final var provider = new SHA1HashProvider(SHA1HashProviderFactory.ID); + var expected = "2aae6c35c94fcfb415dbe95f408b9ce91ee846ed"; + var encoded = provider.encode("hello world", 0); + assertTrue(encoded.equals(expected)); + } + + @Test + public void encodeEmptyString() { + final var provider = new SHA1HashProvider(SHA1HashProviderFactory.ID); + var expected = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; + var encoded = provider.encode("", 0); + assertTrue(encoded.equals(expected)); + } + + @Test + public void ensureIterationParameterIsIgnored() { + final var provider = new SHA1HashProvider(SHA1HashProviderFactory.ID); + var expected = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; + var encoded = provider.encode("", 0); + assertTrue(encoded.equals(expected)); + + expected = "da39a3ee5e6b4b0d3255bfef95601890afd80709"; + encoded = provider.encode("", 42); // any random number + assertTrue(encoded.equals(expected)); + } + + @Test + public void testHashesWithLeadingZeros() { + final var provider = new SHA1HashProvider(SHA1HashProviderFactory.ID); + var expected = "042dc4512fa3d391c5170cf3aa61e6a638f84342"; + var encoded = provider.encode("i", 0); + assertTrue(encoded.equals(expected)); + } +}