From f7f7a07db454946e1c63f3f86fac0ed414677806 Mon Sep 17 00:00:00 2001 From: brontolosone <177225737+brontolosone@users.noreply.github.com> Date: Mon, 21 Oct 2024 18:24:19 +0000 Subject: [PATCH] hash non-numeric input when used as PRNG seed, fixes #800 (#801) --- .../javarosa/core/model/ItemsetBinding.java | 17 +++++++- .../javarosa/xpath/expr/XPathFuncExpr.java | 23 ++++++++++ .../xpath/expr/XPathFuncAsSomethingTest.java | 42 +++++++++++++++++++ 3 files changed, 80 insertions(+), 2 deletions(-) create mode 100644 src/test/java/org/javarosa/xpath/expr/XPathFuncAsSomethingTest.java diff --git a/src/main/java/org/javarosa/core/model/ItemsetBinding.java b/src/main/java/org/javarosa/core/model/ItemsetBinding.java index 5128e56e2..5b6324938 100644 --- a/src/main/java/org/javarosa/core/model/ItemsetBinding.java +++ b/src/main/java/org/javarosa/core/model/ItemsetBinding.java @@ -2,6 +2,7 @@ import static org.javarosa.core.model.FormDef.getAbsRef; import static org.javarosa.xform.parse.RandomizeHelper.shuffle; +import static org.javarosa.xpath.expr.XPathFuncExpr.toLongHash; import static org.javarosa.xpath.expr.XPathFuncExpr.toNumeric; import java.io.DataInputStream; @@ -36,6 +37,7 @@ import org.javarosa.model.xform.XPathReference; import org.javarosa.xpath.XPathConditional; import org.javarosa.xpath.XPathException; +import org.javarosa.xpath.XPathNodeset; import org.javarosa.xpath.expr.XPathNumericLiteral; import org.javarosa.xpath.expr.XPathPathExpr; @@ -309,10 +311,21 @@ private static MultipleItemsData getFilteredAndBoundSelections(MultipleItemsData } private Long resolveRandomSeed(DataInstance model, EvaluationContext ec) { + XPathNodeset seedNode = null; if (randomSeedNumericExpr != null) return ((Double) randomSeedNumericExpr.eval(model, ec)).longValue(); - if (randomSeedPathExpr != null) - return toNumeric(randomSeedPathExpr.eval(model, ec)).longValue(); + if (randomSeedPathExpr != null) { + seedNode = randomSeedPathExpr.eval(model, ec); + Double asDouble = toNumeric(seedNode); + if (asDouble == Double.NaN) { + // Reasonable attempts at reading the node's value as a number failed. + // Fall back to deriving the seed from it using hashing. + // See https://github.com/getodk/javarosa/issues/800 + return toLongHash(seedNode); + } else { + return asDouble.longValue(); + } + } return null; } diff --git a/src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java b/src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java index d3edd19d2..a0aaa8bec 100644 --- a/src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java +++ b/src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java @@ -22,6 +22,8 @@ import java.io.DataOutputStream; import java.io.IOException; import java.math.BigDecimal; +import java.nio.ByteBuffer; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.Date; @@ -56,6 +58,7 @@ import org.javarosa.xpath.XPathUnhandledException; import org.jetbrains.annotations.NotNull; import org.joda.time.DateTime; +import org.bouncycastle.crypto.digests.SHA256Digest; /** * Representation of an xpath function expression. @@ -699,6 +702,26 @@ public static Double toDouble(Object o) { } + /** + * convert a string value to an integer by: + * - encoding it as utf-8 + * - hashing it with sha256 (available cross-platform, including via browser crypto API) + * - interpreting the first 8 bytes of the hash as a long + */ + public static long toLongHash(String sourceString) { + byte[] hasheeBuf = sourceString.getBytes(Charset.forName("UTF-8")); + SHA256Digest hasher = new SHA256Digest(); + hasher.update(hasheeBuf, 0, hasheeBuf.length); + byte[] digestBuf = new byte[32]; + hasher.doFinal(digestBuf, 0); + return (ByteBuffer.wrap(digestBuf)).getLong(0); + } + + public static long toLongHash(Object o) { + String sourceString = (String) unpack(o); + return toLongHash(sourceString); + } + /** * convert a value to a number using xpath's type conversion rules (note that xpath itself makes * no distinction between integer and floating point numbers) diff --git a/src/test/java/org/javarosa/xpath/expr/XPathFuncAsSomethingTest.java b/src/test/java/org/javarosa/xpath/expr/XPathFuncAsSomethingTest.java new file mode 100644 index 000000000..03aa535d1 --- /dev/null +++ b/src/test/java/org/javarosa/xpath/expr/XPathFuncAsSomethingTest.java @@ -0,0 +1,42 @@ +package org.javarosa.xpath.expr; + +import org.junit.Test; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.not; +import static org.javarosa.xpath.expr.XPathFuncExpr.toLongHash; +import static org.javarosa.xpath.expr.XPathFuncExpr.toNumeric; + +import java.util.Date; + +public class XPathFuncAsSomethingTest { + + @Test + public void toLongHashHashesWell() { + assertThat(toLongHash("Hello"), equalTo(1756278180214341157L)); + assertThat(toLongHash(""), equalTo(-2039914840885289964L)); + } + + @Test + public void toNumericHandlesBooleans() { + assertThat(toNumeric(true), equalTo(1.0)); + assertThat(toNumeric(false), equalTo(0.0)); + } + + @Test + public void toNumericHandlesStrings() { + assertThat(toNumeric(" 123 "), equalTo(123.0)); + assertThat(toNumeric(" 123.0 "), equalTo(123.0)); + assertThat(toNumeric(" 123.4 "), equalTo(123.4)); + assertThat(toNumeric(" 123,4 "), equalTo(123.4)); + + assertThat(toNumeric("0x12"), not(18.0)); + assertThat(toNumeric("0x12"), equalTo(Double.NaN)); + } + + @Test + public void toNumericHandlesDates() { + assertThat(toNumeric(new Date(86400 * 1000L)), equalTo(1.0)); + } +} \ No newline at end of file