Skip to content

Commit

Permalink
hash non-numeric input when used as PRNG seed, fixes #800 (#801)
Browse files Browse the repository at this point in the history
  • Loading branch information
brontolosone authored Oct 21, 2024
1 parent 6ce1352 commit f7f7a07
Show file tree
Hide file tree
Showing 3 changed files with 80 additions and 2 deletions.
17 changes: 15 additions & 2 deletions src/main/java/org/javarosa/core/model/ItemsetBinding.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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;

Expand Down Expand Up @@ -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;
}

Expand Down
23 changes: 23 additions & 0 deletions src/main/java/org/javarosa/xpath/expr/XPathFuncExpr.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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));
}
}

0 comments on commit f7f7a07

Please sign in to comment.