Skip to content

Commit

Permalink
🎉 initial commit.
Browse files Browse the repository at this point in the history
  • Loading branch information
matthewhartstonge committed Nov 8, 2021
0 parents commit d685cd9
Show file tree
Hide file tree
Showing 6 changed files with 375 additions and 0 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
.idea
**/.DS_Store
target
31 changes: 31 additions & 0 deletions README.md
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.
88 changes: 88 additions & 0 deletions pom.xml
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>
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));
}
}
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);
}
}
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)));
}
}

0 comments on commit d685cd9

Please sign in to comment.