-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
0 parents
commit d685cd9
Showing
6 changed files
with
375 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
.idea | ||
**/.DS_Store | ||
target |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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. |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,88 @@ | ||
<?xml version="1.0" encoding="UTF-8"?> | ||
<project xmlns="http://maven.apache.org/POM/4.0.0" | ||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" | ||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> | ||
<modelVersion>4.0.0</modelVersion> | ||
|
||
<groupId>nz.mykro</groupId> | ||
<artifactId>fusionauth-argon2id-password-encryptor</artifactId> | ||
<version>0.1.0</version> | ||
|
||
<properties> | ||
<maven.compiler.source>11</maven.compiler.source> | ||
<maven.compiler.target>11</maven.compiler.target> | ||
</properties> | ||
|
||
|
||
<dependencies> | ||
<dependency> | ||
<groupId>com.google.inject</groupId> | ||
<artifactId>guice</artifactId> | ||
<version>4.2.3</version> | ||
<scope>compile</scope> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>com.google.inject.extensions</groupId> | ||
<artifactId>guice-multibindings</artifactId> | ||
<version>4.2.3</version> | ||
<scope>compile</scope> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.bouncycastle</groupId> | ||
<artifactId>bcprov-jdk15on</artifactId> | ||
<version>1.69</version> | ||
<scope>compile</scope> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>io.fusionauth</groupId> | ||
<artifactId>fusionauth-plugin-api</artifactId> | ||
<version>1.15.8</version> | ||
<scope>provided</scope> | ||
</dependency> | ||
|
||
<dependency> | ||
<groupId>org.testng</groupId> | ||
<artifactId>testng</artifactId> | ||
<version>6.14.3</version> | ||
<scope>test</scope> | ||
</dependency> | ||
</dependencies> | ||
|
||
<build> | ||
<plugins> | ||
<plugin> | ||
<groupId>org.apache.maven.plugins</groupId> | ||
<artifactId>maven-shade-plugin</artifactId> | ||
<version>3.2.4</version> | ||
<executions> | ||
<execution> | ||
<phase>package</phase> | ||
<goals> | ||
<goal>shade</goal> | ||
</goals> | ||
|
||
<configuration> | ||
<minimizeJar>true</minimizeJar> | ||
<shadedArtifactAttached>true</shadedArtifactAttached> | ||
<artifactSet> | ||
<includes> | ||
<include>org.bouncycastle:bcprov-jdk15on</include> | ||
</includes> | ||
</artifactSet> | ||
|
||
<transformers> | ||
<!-- exclude dependency manifest files --> | ||
<transformer implementation="org.apache.maven.plugins.shade.resource.DontIncludeResourceTransformer"> | ||
<resource>MANIFEST.MF</resource> | ||
</transformer> | ||
</transformers> | ||
</configuration> | ||
</execution> | ||
</executions> | ||
</plugin> | ||
</plugins> | ||
</build> | ||
</project> |
156 changes: 156 additions & 0 deletions
156
src/main/java/nz/mykro/fusionauth/plugins/Argon2idPasswordEncryptor.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
/* | ||
* Copyright (c) 2021. Matthew Hartstonge <matt@mykro.co.nz> | ||
* | ||
* 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 <a href="https://datatracker.ietf.org/doc/html/rfc9106"> RFC9106: Argon2 Memory-Hard Function for Password Hashing and Proof-of-Work Applications </a> | ||
* @see <a href="https://github.com/P-H-C/phc-winner-argon2/blob/master/argon2-specs.pdf"> Original Argon2 Whitepaper </a> | ||
* @see <a href="https://www.bouncycastle.org/docs/docs1.5on/org/bouncycastle/crypto/params/Argon2Parameters.Builder.html"> Bouncy Castle Argon2Parameters </a> | ||
* @see <a href="https://www.bouncycastle.org/docs/docs1.5on/org/bouncycastle/crypto/generators/Argon2BytesGenerator.htmll"> Bouncy Castle Argon2BytesGenerator </a> | ||
*/ | ||
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. | ||
* <p> | ||
* 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: | ||
* <ul> | ||
* <li>{X} is the argon2 variant (`i`, `d`, or `id`)</li> | ||
* <li>{V} is the argon2 version as an integer, which when rendered in hexadecimal denotes the argon2 version. | ||
* For example, `v=19` equals `0x13`, that is, Argon2 1.3.</li> | ||
* <li>{M} is the memory cost in kibibytes.</li> | ||
* <li>{T} is the time cost in linear iterations.</li> | ||
* <li>{P} is the amount of parallelism, that is, how many 'lanes' or threads are spawned.</li> | ||
* <li>{salt} is the base64 encoded version of the salt bytes.</li> | ||
* <li>{digest} is the base64 encoded version of the derived key bytes.</li> | ||
* </ul> | ||
* | ||
* @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)); | ||
} | ||
} |
42 changes: 42 additions & 0 deletions
42
src/main/java/nz/mykro/fusionauth/plugins/guice/Argon2idFusionAuthPluginModule.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
/* | ||
* Copyright (c) 2021. Matthew Hartstonge <matt@mykro.co.nz> | ||
* | ||
* 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<String, PasswordEncryptor> encryptorMapBinder = MapBinder | ||
.newMapBinder(binder(), String.class, PasswordEncryptor.class); | ||
|
||
encryptorMapBinder | ||
.addBinding("Salted Argon2id") | ||
.to(Argon2idPasswordEncryptor.class); | ||
} | ||
} |
55 changes: 55 additions & 0 deletions
55
src/test/java/nz/mykro/fusionauth/plugins/Argon2idPasswordEncryptorTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,55 @@ | ||
/* | ||
* Copyright (c) 2021. Matthew Hartstonge <matt@mykro.co.nz> | ||
* | ||
* 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))); | ||
} | ||
} |