0

I have this function to generate what feels like a random number from a sequence:

const fetch = (x, o) => {
  if (x >= o) {
    return x
  } else {
    const v = (x * x) % o
    return (x <= o / 2) ? v : o - v
  }
}

const fetch16 = (x) => fetch(x, 65519)
const fetch8 = (x) => fetch(x, 251)

// the last number can be anything.
const build16 = (x, o) => fetch16((fetch16(x) + o) % 65536 ^ 42703)
const build8 = (x, o) => fetch8((fetch8(x) + o) % 256 ^ 101)

const j = 115; // If you don't want duplicates, either i or j should stay fixed
let i = 0
let invalid = [];
let valid = new Set;
while (i <= 255) { // <-- small fix here!
    let x = build8(i, j); // To test, you can swap i and j here, and run again.
    if (x > 255) {
        invalid.push([ i, j, x ]);
    } else {
        valid.add(x);
    }
    i++;
}

console.log("invalid:", JSON.stringify(invalid));
console.log("count of valid:", valid.size);

This isn't a real PRNG, check out the linked post for theory. This takes an incrementing sequence and generates what feels like random integers from the incrementing sequence. That is, it maps the incrementing sequence to seemingly random outputs, yet it follows a pattern! The pattern is hard to see from looking at the outputs, but it's all in the theory. The "random" output never repeats for the length of the sequence (256 values for 8-bit integer sequence starting from 0 to 255 for build8).

Given that, how can you then take the output of this function and get back the original input? Assume we know the j plugged in originally to generate the original output. Given you know j and have the output number, how do you get back the original number? That is, how do you reverse this build8 or build16 function?

I am stuck at the beginning, I don't know the theory for how to reverse a function implementation like this. If you know the theory and can explain that, maybe that would help me try it on my own, but as of now I would be shooting in the dark and wondering if it's simple if you already know the theory.

We can simplify the problem by saying we know what o is in the build8 function, that is always going to be fixed and known in both forward and inverse/reverse versions. So we can get rid of o and just leave x.

const build8b = (x) => fetch8((fetch8(x) + 123) % 256 ^ 101)

The question is, how do you find what x is given the output from build8b?

console.log(build8b(100)) // => 92
// then this is what we need to figure out how to implement:
console.log(reverse8b(92)) // => 100

If it's not possible to find the inverse, then that would be a good answer to know as well (and why), then I can stop searching for a solution.

Lance
  • 75,200
  • 93
  • 289
  • 503
  • PRNG are usually hashing functions and, as such, aren't reverse-able. For example, there's no inverse of the modulus (`%`) operator. See: One-Way Function. – Ouroborus Aug 19 '21 at 15:55
  • This isn't like a regular PRNG, this is a one-to-one mapping function but it _feels_ random. See [here](https://preshing.com/20121224/how-to-generate-a-sequence-of-unique-random-integers/) how it works. That's why I said "pseudo-pseudo RNG". – Lance Aug 19 '21 at 15:58
  • In that case, the most direct way is to actualize the map: Build an array whose indexes are the input value and whose cells contain the output values. – Ouroborus Aug 19 '21 at 16:01
  • Can't it be done in an equation way? – Lance Aug 19 '21 at 16:07
  • 2
    @LancePollard Where have you gotten stuck in trying to do it? – Thomas Jager Aug 19 '21 at 16:08
  • I am stuck at the beginning, I don't even know where to begin. This can't be done by writing all values in advance, that defeats the purpose. – Lance Aug 19 '21 at 16:10
  • 3
    At the core of it, the formula you're trying to reverse is `y = x * x % p`. Mathematically, this is `x^2 = y (mod p)` where `p` is an odd prime. [Complexity of finding square roots](https://en.wikipedia.org/wiki/Quadratic_residue#Complexity_of_finding_square_roots) talks about this but, to summarize, there's no inverse, it requires a search. – Ouroborus Aug 19 '21 at 16:28
  • @Ouroborus: it requires a probabilistic algorithm but it is almost always very fast. In practice, computing square roots mod a prime is easy. – President James K. Polk Aug 19 '21 at 16:35
  • I'm unclear as to what we're supposed to try to reverse. You say build8 and build16 in one place but you have a loop over `i` and I don't know what that's about. Also, there are two inputs to the buildX functions so it's unclear what is known to the attacker and what success is defined as. – President James K. Polk Aug 19 '21 at 16:47
  • @PresidentJamesK.Polk this isn't a real PRNG, there are no attackers, this is for "fake random ID generation". I want to reverse `build8` (or `build16`). The `i` and `j` are for demonstrating inputs/outputs. `j` is a fixed/known input in the forward and backward version. `build16` has 2 inputs, but `o` (put in as `j`) is fixed, so all we are trying to find is what is the input `x`? – Lance Aug 19 '21 at 18:00
  • @Ouroborus when you say it requires a search, what kind of search? Do I need to precompute every input/output and use a hash/map to find it, or is there something more clever? – Lance Aug 19 '21 at 18:12
  • 2
    PRNG can be seen as a hashing mechanism. They're not supposed to be reversable. So the "attacker" is some hypothetical entity that wants to reverse it. Based on what @PresidentJamesK.Polk commented, there's likely a clever solution to prune what would otherwise be a brute-force search so much so as to make it trivial in most cases. However, I don't know enough about the mathematics to research that, just enough to know that Polk is likely correct. – Ouroborus Aug 19 '21 at 23:18

1 Answers1

1

Roughly, the squaring here is invertible because it's possible to compute square roots modulo a prime. If the input value x you get is not a quadratic residue, then p - x will be.

You can therefore invert most of these operations except the folding in half that's been done at the top. I've included some not-quite-working code below which demonstrates the basic concept.

As a note, this is not a good CSPRNG because we assume that the attacker knows the algorithm we used to generate it. It may be good enough for non-cryptographic purposes, but generally for those purposes ChaCha8 is both faster and produces better output, since ChaCha8 is presently considered cryptographically secure (but just barely so), and for cryptographic purposes ChaCha12 or ChaCha20 is better. If you want a permutation of a small set of objects, use one of those PRNGs with a Fisher-Yates shuffle.

Here's the code. test8 and testinv8 show the operation without the final square-and-fold operation, which shows why it's not completely invertible.

const fetch = (x, o) => {
  if (x >= o) {
    return x
  } else {
    const v = (x * x) % o
    return (x <= o / 2) ? v : o - v
  }
}

const powmod = (a, k, p) => {
    let t = a
    let x = 1
    while (k != 0) {
        if (k & 1) {
            x *= t
            x %= p
        }
        t = (t * t) % p
        k >>= 1
    }
    return x
}

const euler = (a, p) => powmod(a, (p - 1) / 2, p)

const sqrt = (z, p) => {
    if (z >= p) {
        return z
    }

    const e = euler(z, p)
    if (e != 1) {
        z = p - z
    }

    const k = (p - 3) / 4
    const v = powmod(z, k + 1, p)
    return (v <= p / 2) ? v : p - v
}

const sqrt8 = (x) => sqrt(x, 251)

const fetch16 = (x) => fetch(x, 65519)
const fetch8 = (x) => fetch(x, 251)

// the last number can be anything.
const build16 = (x, o) => fetch16((fetch16(x) + o) % 65536 ^ 42703)
const build8 = (x, o) => fetch8((fetch8(x) + o) % 256 ^ 101)
const test8 = (x, o) => ((fetch8(x) + o) % 256)
const testinv8 = (x, o) => sqrt8((256 + x - o) % 256)

const inv8 = (x, o) => sqrt8(((sqrt8(x) ^ 101) + (256 - o)) % 256)

const j = 115; // If you don't want duplicates, either i or j should stay fixed
let i = 0
let invalid = [];
let valid = new Set;
while (i <= 255) { // <-- small fix here!
    let x = build8(i, j); // To test, you can swap i and j here, and run again.
    let y = inv8(x, j)
    let z = test8(i, j)
    let a = testinv8(z, j)
    if (x > 255) {
        invalid.push([ i, j, x ]);
    } else {
        valid.add(x);
        console.log("item ", i, ", ", x, ", ", y, ", ", z, ", ", a)
    }
    i++;
}

console.log("invalid:", JSON.stringify(invalid));
console.log("count of valid:", valid.size);
bk2204
  • 64,793
  • 6
  • 84
  • 100