Lance Pollard
Lance Pollard

Reputation: 79440

How can you reverse this pseudo "PRNG" to get back the original number?

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.

Upvotes: 0

Views: 176

Answers (1)

bk2204
bk2204

Reputation: 76874

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);

Upvotes: 1

Related Questions