Improved JNI error handling.

Added new verify overload which determines type from encoded.
This commit is contained in:
Alan Evans 2020-01-23 13:00:42 -05:00 committed by GitHub
parent 6da4513f13
commit 90f34fcc9c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
15 changed files with 290 additions and 60 deletions

View File

@ -6,6 +6,7 @@ android {
defaultConfig {
minSdkVersion 19
targetSdkVersion 28
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

View File

@ -1,7 +1,7 @@
apply plugin: 'maven'
apply plugin: 'signing'
version = '13.0'
version = '13.1'
group = 'org.signal'
archivesBaseName = 'argon2'

View File

@ -1,9 +1,11 @@
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include "org_signal_argon2_Argon2Native.h"
#include "argon2.h"
#define ENCODED_LEN 512
#define SIGNAL_ERROR_NULL_INPUT -100
#define SIGNAL_ERROR_BUFFER_ALLOCATION -101
#define SIGNAL_ERROR_JNI_METHOD -102
JNIEXPORT jint JNICALL Java_org_signal_argon2_Argon2Native_hash
(JNIEnv *env,
@ -18,54 +20,89 @@ JNIEXPORT jint JNICALL Java_org_signal_argon2_Argon2Native_hash
jint argon_type,
jint version)
{
jsize pwd_size = (*env)->GetArrayLength(env, jPwd);
jsize salt_size = (*env)->GetArrayLength(env, jSalt);
jsize outLen = (*env)->GetArrayLength(env, jHash);
jbyte* pwdElements = (*env)->GetByteArrayElements(env, jPwd, NULL);
jbyte* saltElements = (*env)->GetByteArrayElements(env, jSalt, NULL);
if (jPwd == NULL) return SIGNAL_ERROR_NULL_INPUT;
if (jSalt == NULL) return SIGNAL_ERROR_NULL_INPUT;
if (jHash == NULL) return SIGNAL_ERROR_NULL_INPUT;
unsigned char out[outLen];
char encoded[ENCODED_LEN];
jsize pwd_size = (*env)->GetArrayLength(env, jPwd);
jsize salt_size = (*env)->GetArrayLength(env, jSalt);
jsize hash_size = (*env)->GetArrayLength(env, jHash);
int ret = argon2_hash(t, m, parallelism,
pwdElements, pwd_size,
saltElements, salt_size,
out, outLen,
encoded, ENCODED_LEN,
argon_type,
version);
char * encoded = NULL;
jbyte* pwdElements = NULL;
jbyte* saltElements = NULL;
jbyte* hashElements = NULL;
(*env)->ReleaseByteArrayElements(env, jPwd, pwdElements, JNI_ABORT);
(*env)->ReleaseByteArrayElements(env, jSalt, saltElements, JNI_ABORT);
const int encoded_size = jEncoded == NULL ? 0 : 512;
if (ret == ARGON2_OK) {
(*env)->SetByteArrayRegion(env, jHash, 0, outLen, (jbyte *)out);
if (encoded_size > 0) encoded = (char *) malloc(encoded_size);
if (encoded_size == 0 || encoded != NULL) pwdElements = (*env)->GetByteArrayElements(env, jPwd, NULL);
if (pwdElements != NULL) saltElements = (*env)->GetByteArrayElements(env, jSalt, NULL);
if (saltElements != NULL) hashElements = (*env)->GetByteArrayElements(env, jHash, NULL);
int result = (encoded_size > 0 && encoded == NULL) || pwdElements == NULL || saltElements == NULL || hashElements == NULL
? SIGNAL_ERROR_BUFFER_ALLOCATION
: argon2_hash(t, m, parallelism,
pwdElements, pwd_size,
saltElements, salt_size,
hashElements, hash_size,
encoded, encoded_size,
argon_type,
version);
if (result == ARGON2_OK && jEncoded != NULL && encoded != NULL) {
jclass stringBufferClass = (*env)->GetObjectClass(env, jEncoded);
jmethodID appendMethod = (*env)->GetMethodID(env, stringBufferClass, "append", "(Ljava/lang/String;)Ljava/lang/StringBuffer;");
(*env)->CallObjectMethod(env, jEncoded, appendMethod, (*env)->NewStringUTF(env, encoded));
if (appendMethod == NULL) {
result = SIGNAL_ERROR_JNI_METHOD;
} else {
(*env)->CallObjectMethod(env, jEncoded, appendMethod, (*env)->NewStringUTF(env, encoded));
}
}
return ret;
if (pwdElements != NULL) (*env)->ReleaseByteArrayElements(env, jPwd, pwdElements, JNI_ABORT);
if (saltElements != NULL) (*env)->ReleaseByteArrayElements(env, jSalt, saltElements, JNI_ABORT);
if (hashElements != NULL) (*env)->ReleaseByteArrayElements(env, jHash, hashElements, result == ARGON2_OK ? 0 : JNI_ABORT);
if (encoded != NULL) free(encoded);
return result;
}
JNIEXPORT jint JNICALL Java_org_signal_argon2_Argon2Native_verify
(JNIEnv *env, jclass clazz, jstring jEncoded, jbyteArray jPwd, jint argon_type)
{
const char *encoded = (*env)->GetStringUTFChars(env, jEncoded, NULL);
jsize pwd_size = (*env)->GetArrayLength(env, jPwd);
jbyte *pwd = (*env)->GetByteArrayElements(env, jPwd, NULL);
if (jEncoded == NULL) return SIGNAL_ERROR_NULL_INPUT;
if (jPwd == NULL) return SIGNAL_ERROR_NULL_INPUT;
int ret = argon2_verify((char *)encoded, pwd, pwd_size, argon_type);
const char *encoded = NULL;
(*env)->ReleaseByteArrayElements(env, jPwd, pwd, JNI_ABORT);
(*env)->ReleaseStringUTFChars(env, jEncoded, encoded);
jsize pwd_size = (*env)->GetArrayLength(env, jPwd);
jbyte *pwd = (*env)->GetByteArrayElements(env, jPwd, NULL);
return ret;
if (pwd != NULL) encoded = (*env)->GetStringUTFChars(env, jEncoded, NULL);
int result = pwd == NULL || encoded == NULL
? SIGNAL_ERROR_BUFFER_ALLOCATION
: argon2_verify((char *)encoded, pwd, pwd_size, argon_type);
if (pwd != NULL) (*env)->ReleaseByteArrayElements(env, jPwd, pwd, JNI_ABORT);
if (encoded != NULL) (*env)->ReleaseStringUTFChars(env, jEncoded, encoded);
return result;
}
JNIEXPORT jstring JNICALL Java_org_signal_argon2_Argon2Native_resultToString
(JNIEnv *env, jclass clazz, jint argonResult)
{
return (*env)->NewStringUTF(env, argon2_error_message(argonResult));
const char *message;
switch (argonResult) {
case SIGNAL_ERROR_NULL_INPUT: message = "Input parameter was NULL"; break;
case SIGNAL_ERROR_BUFFER_ALLOCATION: message = "Failed to allocate input buffers"; break;
case SIGNAL_ERROR_JNI_METHOD: message = "Failed to find method"; break;
default: message = argon2_error_message(argonResult);
}
return (*env)->NewStringUTF(env, message);
}

Binary file not shown.

View File

@ -0,0 +1,52 @@
package org.signal.argon2;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.signal.argon2.TestUtils.utf8;
import static org.signal.argon2.Type.Argon2id;
public final class Argon2BadParameterTest {
@Test
public void null_password() {
Argon2 argon2 = new Argon2.Builder(Version.V13)
.type(Argon2id)
.memoryCost(MemoryCost.MiB(32))
.parallelism(1)
.iterations(1)
.build();
//noinspection ConstantConditions
assertThatThrownBy(()-> argon2.hash(null, utf8("somesalt")))
.isExactlyInstanceOf(IllegalArgumentException.class);
}
@Test
public void null_salt() {
Argon2 argon2 = new Argon2.Builder(Version.V13)
.type(Argon2id)
.memoryCost(MemoryCost.MiB(32))
.parallelism(1)
.iterations(1)
.build();
//noinspection ConstantConditions
assertThatThrownBy(() -> argon2.hash(utf8("signal"), null))
.isExactlyInstanceOf(IllegalArgumentException.class);
}
@Test
public void verify_with_null_password() {
//noinspection ConstantConditions
assertThatThrownBy(() ->
Argon2.verify("$argon2id$v=19$m=32768,t=1,p=1$c29tZXNhbHQ$5d38aTyOwp6kx3ALaN/k04OsQ98uO6FRLo5XYsy9gZ4", null, Argon2id)
).isExactlyInstanceOf(IllegalArgumentException.class);
}
@Test
public void verify_with_null_encoded() {
//noinspection ConstantConditions
assertThatThrownBy(() ->
Argon2.verify(null, utf8("signal"), Argon2id)
).isExactlyInstanceOf(IllegalArgumentException.class);
}
}

View File

@ -6,6 +6,7 @@ import org.junit.Test;
import org.junit.runner.RunWith;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertEquals;
import static org.signal.argon2.TestUtils.utf8;
import static org.signal.argon2.Type.Argon2id;
@ -32,25 +33,33 @@ public final class Argon2BuilderTest {
}
@Test
public void using_MemoryCost() throws Argon2Exception {
String hash1 = new Argon2.Builder(Version.V13)
.type(Argon2id)
.memoryCost(MemoryCost.MiB(20))
.parallelism(1)
.iterations(1)
.build()
.hash(utf8("signal"), utf8("somesalt"))
.getEncoded();
public void using_MemoryCost_object() throws Argon2Exception {
String hash = new Argon2.Builder(Version.V13)
.type(Argon2id)
.memoryCost(MemoryCost.MiB(32))
.parallelism(1)
.iterations(1)
.build()
.hash(utf8("signal"), utf8("somesalt"))
.getEncoded();
String hash2 = new Argon2.Builder(Version.V13)
.type(Argon2id)
.memoryCostKiB(20 * 1024)
.parallelism(1)
.iterations(1)
.build()
.hash(utf8("signal"), utf8("somesalt"))
.getEncoded();
assertEquals("$argon2id$v=19$m=32768,t=1,p=1$c29tZXNhbHQ$5d38aTyOwp6kx3ALaN/k04OsQ98uO6FRLo5XYsy9gZ4", hash);
}
assertEquals(hash1, hash2);
@Test
public void password_and_salt_are_unmodified() throws Argon2Exception {
byte[] password = utf8("signal");
byte[] salt = utf8("somesalt");
new Argon2.Builder(Version.V13)
.type(Argon2id)
.memoryCost(MemoryCost.MiB(1))
.parallelism(1)
.iterations(1)
.build()
.hash(password, salt);
assertArrayEquals(utf8("signal"), password);
assertArrayEquals(utf8("somesalt"), salt);
}
}

View File

@ -0,0 +1,40 @@
package org.signal.argon2;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertEquals;
import static org.signal.argon2.Type.Argon2d;
import static org.signal.argon2.Type.Argon2i;
import static org.signal.argon2.Type.Argon2id;
public final class TypeTest {
@Test
public void can_parse_argon2id() throws UnknownTypeException {
assertEquals(Argon2id, Type.fromEncoded("$argon2id$v=19$m=32768,t=1,p=1$c29tZXNhbHQ$5d38aTyOwp6kx3ALaN/k04OsQ98uO6FRLo5XYsy9gZ4"));
}
@Test
public void can_parse_argon2i() throws UnknownTypeException {
assertEquals(Argon2i, Type.fromEncoded("$argon2i$v=19$m=1024,t=1,p=1$c29tZXNhbHQ$F1TwhdCXduq6IqFH+gob2M5rpSok5w2c1YdaTKm6wvw"));
}
@Test
public void can_parse_argon2d() throws UnknownTypeException {
assertEquals(Argon2d, Type.fromEncoded("$argon2d$v=19$m=1024,t=1,p=1$c29tZXNhbHQ$AQZ5maf/c48fwRjlcN4vwK7AXDAwtfwe7MTYI8+27T8"));
}
@Test
public void cant_parse_unknown() {
assertThatThrownBy(() -> Type.fromEncoded("$argon2idx$v=19$m=1024,t=1,p=1$c29tZXNhbHQ$AQZ5maf/c48fwRjlcN4vwK7AXDAwtfwe7MTYI8+27T8"))
.isExactlyInstanceOf(UnknownTypeException.class);
}
@Test
public void cant_parse_null() {
//noinspection ConstantConditions
assertThatThrownBy(() -> Type.fromEncoded(null))
.isExactlyInstanceOf(IllegalArgumentException.class);
}
}

View File

@ -0,0 +1,49 @@
package org.signal.argon2;
import org.junit.Test;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.Assert.assertTrue;
import static org.signal.argon2.TestUtils.utf8;
import static org.signal.argon2.Type.Argon2d;
import static org.signal.argon2.Type.Argon2i;
import static org.signal.argon2.Type.Argon2id;
public final class VerifyTests {
@Test
public void verify_argon2d_specifying_type() {
assertTrue(Argon2.verify("$argon2d$v=19$m=1024,t=1,p=1$c29tZXNhbHQ$AQZ5maf/c48fwRjlcN4vwK7AXDAwtfwe7MTYI8+27T8", utf8("signal"), Argon2d));
}
@Test
public void verify_argon2d_not_specifying_type() throws UnknownTypeException {
assertTrue(Argon2.verify("$argon2d$v=19$m=1024,t=1,p=1$c29tZXNhbHQ$AQZ5maf/c48fwRjlcN4vwK7AXDAwtfwe7MTYI8+27T8", utf8("signal")));
}
@Test
public void verify_argon2i_specifying_type() {
assertTrue(Argon2.verify("$argon2i$v=19$m=1024,t=1,p=1$c29tZXNhbHQ$F1TwhdCXduq6IqFH+gob2M5rpSok5w2c1YdaTKm6wvw", utf8("signal"), Argon2i));
}
@Test
public void verify_argon2i_not_specifying_type() throws UnknownTypeException {
assertTrue(Argon2.verify("$argon2i$v=19$m=1024,t=1,p=1$c29tZXNhbHQ$F1TwhdCXduq6IqFH+gob2M5rpSok5w2c1YdaTKm6wvw", utf8("signal")));
}
@Test
public void verify_argon2id_specifying_type() {
assertTrue(Argon2.verify("$argon2id$v=19$m=32768,t=1,p=1$c29tZXNhbHQ$5d38aTyOwp6kx3ALaN/k04OsQ98uO6FRLo5XYsy9gZ4", utf8("signal"), Argon2id));
}
@Test
public void verify_argon2id_not_specifying_type() throws UnknownTypeException {
assertTrue(Argon2.verify("$argon2id$v=19$m=32768,t=1,p=1$c29tZXNhbHQ$5d38aTyOwp6kx3ALaN/k04OsQ98uO6FRLo5XYsy9gZ4", utf8("signal")));
}
@Test
public void unknown() {
assertThatThrownBy(() -> Argon2.verify("$argon2dx$v=19$m=1024,t=1,p=1$c29tZXNhbHQ$AQZ5maf/c48fwRjlcN4vwK7AXDAwtfwe7MTYI8+27T8", utf8("signal")))
.isExactlyInstanceOf(UnknownTypeException.class);
}
}

View File

@ -20,8 +20,24 @@ public final class Argon2 {
this.version = builder.version;
}
/**
* Finds the type from the encoded hash.
* @param encoded
* @param password
* @return
* @throws UnknownTypeException If it cannot determine the type from the encoded hash.
*/
public static boolean verify(String encoded, byte[] password) throws UnknownTypeException {
return verify(encoded, password, Type.fromEncoded(encoded));
}
public static boolean verify(String encoded, byte[] password, Type type) {
return Argon2Native.verify(encoded, password, type.nativeValue) == Argon2Native.OK;
if (encoded == null) throw new IllegalArgumentException();
if (password == null) throw new IllegalArgumentException();
byte[] defensivePasswordCopy = password.clone();
return Argon2Native.verify(encoded, defensivePasswordCopy, type.nativeValue) == Argon2Native.OK;
}
public static class Builder {
@ -103,15 +119,21 @@ public final class Argon2 {
}
public Result hash(byte[] password, byte[] salt) throws Argon2Exception {
StringBuffer encoded = new StringBuffer();
byte[] hash = new byte[hashLength];
int result = Argon2Native.hash(tCostIterations, mCostKiB, parallelism,
password,
salt,
hash,
encoded,
type.nativeValue,
version.nativeValue);
if (salt == null) throw new IllegalArgumentException();
if (password == null) throw new IllegalArgumentException();
StringBuffer encoded = new StringBuffer();
byte[] hash = new byte[hashLength];
byte[] defensivePasswordCopy = password.clone();
byte[] defensiveSaltCopy = salt.clone();
int result = Argon2Native.hash(tCostIterations, mCostKiB, parallelism,
defensivePasswordCopy,
defensiveSaltCopy,
hash,
encoded,
type.nativeValue,
version.nativeValue);
if (result != Argon2Native.OK) {
throw new Argon2Exception(result, Argon2Native.resultToString(result));

View File

@ -4,7 +4,11 @@ import java.util.Locale;
public final class Argon2Exception extends Exception {
Argon2Exception(String message) {
super(message);
}
Argon2Exception(int nativeErrorValue, String nativeErrorMessage) {
super(String.format(Locale.US, "Argon failed %d: %s", nativeErrorValue, nativeErrorMessage));
this(String.format(Locale.US, "Argon failed %d: %s", nativeErrorValue, nativeErrorMessage));
}
}

View File

@ -13,4 +13,14 @@ public enum Type {
Type(int nativeValue) {
this.nativeValue = nativeValue;
}
public static Type fromEncoded(String encoded) throws UnknownTypeException {
if (encoded == null) throw new IllegalArgumentException();
if (encoded.startsWith("$argon2id$")) return Argon2id;
if (encoded.startsWith("$argon2i$" )) return Argon2i;
if (encoded.startsWith("$argon2d$" )) return Argon2d;
throw new UnknownTypeException();
}
}

View File

@ -0,0 +1,6 @@
package org.signal.argon2;
public final class UnknownTypeException extends Exception {
UnknownTypeException() {
}
}