diff --git a/android/argon2/build.gradle b/android/argon2/build.gradle index b8c4a29..b2496fe 100644 --- a/android/argon2/build.gradle +++ b/android/argon2/build.gradle @@ -6,6 +6,7 @@ android { defaultConfig { minSdkVersion 19 + targetSdkVersion 28 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/android/argon2/deploy.gradle b/android/argon2/deploy.gradle index 8055fae..d94d368 100644 --- a/android/argon2/deploy.gradle +++ b/android/argon2/deploy.gradle @@ -1,7 +1,7 @@ apply plugin: 'maven' apply plugin: 'signing' -version = '13.0' +version = '13.1' group = 'org.signal' archivesBaseName = 'argon2' diff --git a/android/argon2/jni/org_signal_argon2_Argon2Native.c b/android/argon2/jni/org_signal_argon2_Argon2Native.c index aca417c..74888f6 100644 --- a/android/argon2/jni/org_signal_argon2_Argon2Native.c +++ b/android/argon2/jni/org_signal_argon2_Argon2Native.c @@ -1,9 +1,11 @@ #include -#include +#include #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); } diff --git a/android/argon2/libs/arm64-v8a/libargon2.so b/android/argon2/libs/arm64-v8a/libargon2.so index bef5be2..da072ae 100755 Binary files a/android/argon2/libs/arm64-v8a/libargon2.so and b/android/argon2/libs/arm64-v8a/libargon2.so differ diff --git a/android/argon2/libs/armeabi-v7a/libargon2.so b/android/argon2/libs/armeabi-v7a/libargon2.so index f11a770..ebb4459 100755 Binary files a/android/argon2/libs/armeabi-v7a/libargon2.so and b/android/argon2/libs/armeabi-v7a/libargon2.so differ diff --git a/android/argon2/libs/x86/libargon2.so b/android/argon2/libs/x86/libargon2.so index 18df1a3..01c2697 100755 Binary files a/android/argon2/libs/x86/libargon2.so and b/android/argon2/libs/x86/libargon2.so differ diff --git a/android/argon2/libs/x86_64/libargon2.so b/android/argon2/libs/x86_64/libargon2.so index 2f50467..8e379f6 100755 Binary files a/android/argon2/libs/x86_64/libargon2.so and b/android/argon2/libs/x86_64/libargon2.so differ diff --git a/android/argon2/src/androidTest/java/org/signal/argon2/Argon2BadParameterTest.java b/android/argon2/src/androidTest/java/org/signal/argon2/Argon2BadParameterTest.java new file mode 100644 index 0000000..901bc5f --- /dev/null +++ b/android/argon2/src/androidTest/java/org/signal/argon2/Argon2BadParameterTest.java @@ -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); + } +} diff --git a/android/argon2/src/androidTest/java/org/signal/argon2/Argon2BuilderTest.java b/android/argon2/src/androidTest/java/org/signal/argon2/Argon2BuilderTest.java index 489c4d0..a4407a4 100644 --- a/android/argon2/src/androidTest/java/org/signal/argon2/Argon2BuilderTest.java +++ b/android/argon2/src/androidTest/java/org/signal/argon2/Argon2BuilderTest.java @@ -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); } } diff --git a/android/argon2/src/androidTest/java/org/signal/argon2/TypeTest.java b/android/argon2/src/androidTest/java/org/signal/argon2/TypeTest.java new file mode 100644 index 0000000..80caea8 --- /dev/null +++ b/android/argon2/src/androidTest/java/org/signal/argon2/TypeTest.java @@ -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); + } +} diff --git a/android/argon2/src/androidTest/java/org/signal/argon2/VerifyTests.java b/android/argon2/src/androidTest/java/org/signal/argon2/VerifyTests.java new file mode 100644 index 0000000..e46886f --- /dev/null +++ b/android/argon2/src/androidTest/java/org/signal/argon2/VerifyTests.java @@ -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); + } +} diff --git a/android/argon2/src/main/java/org/signal/argon2/Argon2.java b/android/argon2/src/main/java/org/signal/argon2/Argon2.java index 36feab7..3d04a5f 100644 --- a/android/argon2/src/main/java/org/signal/argon2/Argon2.java +++ b/android/argon2/src/main/java/org/signal/argon2/Argon2.java @@ -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)); diff --git a/android/argon2/src/main/java/org/signal/argon2/Argon2Exception.java b/android/argon2/src/main/java/org/signal/argon2/Argon2Exception.java index a45dc01..e741988 100644 --- a/android/argon2/src/main/java/org/signal/argon2/Argon2Exception.java +++ b/android/argon2/src/main/java/org/signal/argon2/Argon2Exception.java @@ -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)); } } diff --git a/android/argon2/src/main/java/org/signal/argon2/Type.java b/android/argon2/src/main/java/org/signal/argon2/Type.java index 59dcb56..15ff2b0 100644 --- a/android/argon2/src/main/java/org/signal/argon2/Type.java +++ b/android/argon2/src/main/java/org/signal/argon2/Type.java @@ -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(); + } } diff --git a/android/argon2/src/main/java/org/signal/argon2/UnknownTypeException.java b/android/argon2/src/main/java/org/signal/argon2/UnknownTypeException.java new file mode 100644 index 0000000..8a7fd82 --- /dev/null +++ b/android/argon2/src/main/java/org/signal/argon2/UnknownTypeException.java @@ -0,0 +1,6 @@ +package org.signal.argon2; + +public final class UnknownTypeException extends Exception { + UnknownTypeException() { + } +}