Improved JNI error handling.
Added new verify overload which determines type from encoded.
This commit is contained in:
parent
6da4513f13
commit
90f34fcc9c
@ -6,6 +6,7 @@ android {
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 19
|
||||
targetSdkVersion 28
|
||||
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
|
||||
}
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
apply plugin: 'maven'
|
||||
apply plugin: 'signing'
|
||||
|
||||
version = '13.0'
|
||||
version = '13.1'
|
||||
group = 'org.signal'
|
||||
archivesBaseName = 'argon2'
|
||||
|
||||
|
||||
@ -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.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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));
|
||||
|
||||
@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,6 @@
|
||||
package org.signal.argon2;
|
||||
|
||||
public final class UnknownTypeException extends Exception {
|
||||
UnknownTypeException() {
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue
Block a user