From d685cd9e07b0dfa073709c15b71c8e8066f777fe Mon Sep 17 00:00:00 2001 From: Matthew Hartstonge Date: Mon, 8 Nov 2021 14:39:28 +1300 Subject: [PATCH] :tada: initial commit. --- .gitignore | 3 + README.md | 31 ++++ pom.xml | 88 ++++++++++ .../plugins/Argon2idPasswordEncryptor.java | 156 ++++++++++++++++++ .../guice/Argon2idFusionAuthPluginModule.java | 42 +++++ .../Argon2idPasswordEncryptorTest.java | 55 ++++++ 6 files changed, 375 insertions(+) create mode 100644 .gitignore create mode 100644 README.md create mode 100644 pom.xml create mode 100644 src/main/java/nz/mykro/fusionauth/plugins/Argon2idPasswordEncryptor.java create mode 100644 src/main/java/nz/mykro/fusionauth/plugins/guice/Argon2idFusionAuthPluginModule.java create mode 100644 src/test/java/nz/mykro/fusionauth/plugins/Argon2idPasswordEncryptorTest.java diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..570361e --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.idea +**/.DS_Store +target diff --git a/README.md b/README.md new file mode 100644 index 0000000..1f51ce5 --- /dev/null +++ b/README.md @@ -0,0 +1,31 @@ +# fusionauth-argon2id-password-encryptor + +This plugin provides a pure Java implementation for generating argon2id password +digests in FusionAuth. + +## Java Install +To install Java, you will need the OpenJDK. Due to licensing, you can't simply +use the Oracle JDK distribution due to 'commercial use'. + +Hop over to [Adoptium](https://adoptium.net/?variant=openjdk11) to download the +OpenJDK for your platform. + +## Building + +This project uses Maven, so to build the required JAR file, you need to run: + +```bash +$ mvn package +``` + +This will spit out a "shaded" uber JAR, that is a JAR file packaged with all +required dependencies packed into a single JAR file. + +## Deployment +### FusionAuth Cloud +The required JAR file need to be sent on to the FusionAuth's support team via +email or via a support ticket, so they can plug it directly into the specific +cloud deployment. + +### FusionAuth +Take the shaded JAR and copy/paste/add it into FusionAuth's `plugin` folder. diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..078ae03 --- /dev/null +++ b/pom.xml @@ -0,0 +1,88 @@ + + + 4.0.0 + + nz.mykro + fusionauth-argon2id-password-encryptor + 0.1.0 + + + 11 + 11 + + + + + + com.google.inject + guice + 4.2.3 + compile + + + + com.google.inject.extensions + guice-multibindings + 4.2.3 + compile + + + + org.bouncycastle + bcprov-jdk15on + 1.69 + compile + + + + io.fusionauth + fusionauth-plugin-api + 1.15.8 + provided + + + + org.testng + testng + 6.14.3 + test + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 3.2.4 + + + package + + shade + + + + true + true + + + org.bouncycastle:bcprov-jdk15on + + + + + + + MANIFEST.MF + + + + + + + + + diff --git a/src/main/java/nz/mykro/fusionauth/plugins/Argon2idPasswordEncryptor.java b/src/main/java/nz/mykro/fusionauth/plugins/Argon2idPasswordEncryptor.java new file mode 100644 index 0000000..cd701b3 --- /dev/null +++ b/src/main/java/nz/mykro/fusionauth/plugins/Argon2idPasswordEncryptor.java @@ -0,0 +1,156 @@ +/* + * Copyright (c) 2021. Matthew Hartstonge + * + * 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 nz.mykro.fusionauth.plugins; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +import org.bouncycastle.crypto.generators.Argon2BytesGenerator; +import org.bouncycastle.crypto.params.Argon2Parameters; + +import io.fusionauth.plugin.spi.security.PasswordEncryptor; + + +/** + * This is an example of an Argon2id hashing algorithm implemented in pure Java. + * + * @author Matthew Hartstonge + * @see RFC9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications + * @see Original Argon2 Whitepaper + * @see Bouncy Castle Argon2Parameters + * @see Bouncy Castle Argon2BytesGenerator + */ +public class Argon2idPasswordEncryptor implements PasswordEncryptor { + /** + * If much less memory is available, a uniformly safe option is + * Argon2id with t=3 iterations, p=4 lanes, m=2^(16) (64 MiB of + * RAM), 128-bit salt, and 256-bit tag size. This is the SECOND + * RECOMMENDED option. + *

+ * Refer: https://datatracker.ietf.org/doc/html/rfc9106#section-4 + */ + private static final int DEFAULT_TYPE = Argon2Parameters.ARGON2_id; + private static final int DEFAULT_VERSION = Argon2Parameters.ARGON2_VERSION_13; + private static final int DEFAULT_TIME_COST = 3; // t=3 (3 Iterations) + private static final int DEFAULT_MEMORY_COST = 1 << 16; // m=2^16 (64MiB Memory) + private static final int DEFAULT_PARALLELISM = 4; // p=4 (4 Lanes) + private static final int DEFAULT_TAG_SIZE = 1 << 8; // tag size=2^8 (256-bit hash size) + private static final int DEFAULT_OFFSET = 0; + + @Override + public int defaultFactor() { + return DEFAULT_TIME_COST; + } + + @Override + public String encrypt(String password, String salt, int factor) { + if (factor <= 0) { + throw new IllegalArgumentException("Invalid factor value [" + factor + "]"); + } + + Argon2Parameters hasher = new Argon2Parameters + .Builder(DEFAULT_TYPE) + .withVersion(DEFAULT_VERSION) + .withIterations(factor) + .withMemoryAsKB(DEFAULT_MEMORY_COST) + .withParallelism(DEFAULT_PARALLELISM) + .withSalt(b64Decode(salt)) + .build(); + + String hash = this.generateHashString(hasher, generateDigest(hasher, password)); + + // zero out in-memory arrays that may store secrets... + hasher.clear(); + + return hash; + } + + /** + * generateDigest computes the Argon2 hashcode for the password from the provided argon2 configuration. + * + * @param params The configured Argon2 parameters. + * @param password The password to compute a digest hashcode for. + * @return digest The computed Argon2 hashcode from the provided parameters. + */ + private byte[] generateDigest(Argon2Parameters params, String password) { + Argon2BytesGenerator generator = new Argon2BytesGenerator(); + generator.init(params); + + int bytesLength = DEFAULT_TAG_SIZE / 8; + byte[] digest = new byte[bytesLength]; + generator.generateBytes(password.getBytes(StandardCharsets.UTF_8), digest, DEFAULT_OFFSET, bytesLength); + + return digest; + } + + /** + * generateHashString returns an Argon2 string from the configured argon parameters. + * + * The argon2 string has the format `$argon2{X}$v={V}$m={M},t={T},p={P}${salt}${digest}` + * Where: + *

+ * + * @param params The configured Argon2 parameters. + * @param digest The generated Argon2 computed digest hashcode. + * @return hash The argon2 format string representation of the computed digest. + */ + private String generateHashString(Argon2Parameters params, byte[] digest) { + StringBuilder hash = new StringBuilder().append("$argon2"); + switch (params.getType()) { + case Argon2Parameters.ARGON2_d: + hash.append("d"); + break; + + case Argon2Parameters.ARGON2_i: + hash.append("i"); + break; + + case Argon2Parameters.ARGON2_id: + hash.append("id"); + break; + } + + hash.append(String.format( + "$v=%d$m=%d,t=%d,p=%d$%s$%s", + params.getVersion(), + params.getMemory(), + params.getIterations(), + params.getLanes(), + b64Encode(params.getSalt()), + b64Encode(digest) + )); + + return hash.toString(); + } + + private String b64Encode(byte[] in) { + return new String(Base64.getEncoder().withoutPadding().encode(in)); + } + + private byte[] b64Decode(String in) { + return Base64.getDecoder().decode(in.getBytes(StandardCharsets.UTF_8)); + } +} diff --git a/src/main/java/nz/mykro/fusionauth/plugins/guice/Argon2idFusionAuthPluginModule.java b/src/main/java/nz/mykro/fusionauth/plugins/guice/Argon2idFusionAuthPluginModule.java new file mode 100644 index 0000000..bfae66e --- /dev/null +++ b/src/main/java/nz/mykro/fusionauth/plugins/guice/Argon2idFusionAuthPluginModule.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2021. Matthew Hartstonge + * + * 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 nz.mykro.fusionauth.plugins.guice; + +import com.google.inject.AbstractModule; +import com.google.inject.multibindings.MapBinder; +import nz.mykro.fusionauth.plugins.Argon2idPasswordEncryptor; +import io.fusionauth.plugin.spi.PluginModule; +import io.fusionauth.plugin.spi.security.PasswordEncryptor; + +/** + * Argon2idFusionAuthPluginModule binds in a pure Java plugin for creating + * argon2id based + * + * @author Matthew Hartstonge + */ +@PluginModule +public class Argon2idFusionAuthPluginModule extends AbstractModule { + @Override + protected void configure() { + MapBinder encryptorMapBinder = MapBinder + .newMapBinder(binder(), String.class, PasswordEncryptor.class); + + encryptorMapBinder + .addBinding("Salted Argon2id") + .to(Argon2idPasswordEncryptor.class); + } +} diff --git a/src/test/java/nz/mykro/fusionauth/plugins/Argon2idPasswordEncryptorTest.java b/src/test/java/nz/mykro/fusionauth/plugins/Argon2idPasswordEncryptorTest.java new file mode 100644 index 0000000..e025c65 --- /dev/null +++ b/src/test/java/nz/mykro/fusionauth/plugins/Argon2idPasswordEncryptorTest.java @@ -0,0 +1,55 @@ +/* + * Copyright (c) 2021. Matthew Hartstonge + * + * 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 nz.mykro.fusionauth.plugins; + +import io.fusionauth.plugin.spi.security.PasswordEncryptor; +import org.testng.Assert; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +import java.nio.charset.StandardCharsets; +import java.util.Base64; + +/** + * Argon2idPasswordEncryptorTest provides a simple test to ensure hashes are + * being generated as expected given multiple different parameters. + * + * @author Matthew Hartstonge + */ +public class Argon2idPasswordEncryptorTest { + @Test(dataProvider = "hashes") + public void encrypt(String password, String salt, int factor, String expected) { + PasswordEncryptor encryptor = new Argon2idPasswordEncryptor(); + String actual = encryptor.encrypt(password, b64Encode(salt), factor); + Assert.assertEquals(actual, expected); + } + + @DataProvider(name = "hashes") + public Object[][] hashes() { + return new Object[][]{ + {"password123", "saltysalt", 1, "$argon2id$v=19$m=65536,t=1,p=4$c2FsdHlzYWx0$4FnTpZaINA9WWoTmhm1Y5BNP0ueQGQWWPIARybMRN64"}, + {"password123", "saltysalt", 2, "$argon2id$v=19$m=65536,t=2,p=4$c2FsdHlzYWx0$BmD1EnGoAaDtGRiQ4nlW+pGIvHIHT+tW1F8xaWdxDQE"}, + {"password123", "2qUAZD49DpdiOnxQqRHddpBg0Rfb36NM4ZPSDTLCz6cM2MWQx0", 3, "$argon2id$v=19$m=65536,t=3,p=4$MnFVQVpENDlEcGRpT254UXFSSGRkcEJnMFJmYjM2Tk00WlBTRFRMQ3o2Y00yTVdReDA$WFpsLuNnmVrfVO3TRw5p6mtvv3ryDbNzyHzY/CN8/ow"}, + {"password123", "2TR5SPOtQBW62Y1Ju6brUBmd1HlPyfOrGlakmvTUFC5q3JvT1e", 3, "$argon2id$v=19$m=65536,t=3,p=4$MlRSNVNQT3RRQlc2MlkxSnU2YnJVQm1kMUhsUHlmT3JHbGFrbXZUVUZDNXEzSnZUMWU$5P4NBHMf3gSsBbiQc2PVPDUiGNPAT4YWEwOTHwk3aCA"}, + {"password321", "2TR5SPOtQBW62Y1Ju6brUBmd1HlPyfOrGlakmvTUFC5q3JvT1e", 3, "$argon2id$v=19$m=65536,t=3,p=4$MlRSNVNQT3RRQlc2MlkxSnU2YnJVQm1kMUhsUHlmT3JHbGFrbXZUVUZDNXEzSnZUMWU$ZGS+lgDik0LcF2T5m9p7+8OhoH5oztUuo4Po4hruK5w"}, + }; + } + + private String b64Encode(String in) { + return new String(Base64.getEncoder().encode(in.getBytes(StandardCharsets.UTF_8))); + } +}