Skip to content

Implement offsetting to previous internal states #14

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ This cracker works as the following way. It obtains first 624 32 bit numbers fro
It is **important to feed cracker exactly 32-bit integers** generated by the generator due to the fact that they will be generated anyway, but dropped if you don't request for them.
As well, you must feed the cracker exactly after new seed is presented, or after 624*32 bits are generated since every 624 32-bit numbers generator shifts it's state and cracker is designed to be fed from the begining of some state.

#### Implemented methods
### Implemented methods

Cracker has one method for feeding: `submit(n)`. After submitting 624 integers it won't take any more and will be ready for predicting new numbers.

Expand All @@ -54,3 +54,33 @@ print("Random result: {}\nCracker result: {}"
Random result: 127160928
Cracker result: 127160928
```

As well as predicting future values, it can recover the *previous* states to predict earlier values, ones that came before the numbers you submit. After having submitted enough random numbers to clone the internal state (624), you can use the `offset(n)` method to offset the state by some number.

A positive number simply advances the RNG by `n`, as if you would ask for a number repeatedly `n` times. A **negative** number however will *untwist* the internal state (which can also be done manually with `untwist()`). Then after untwisting enough times it will set the internal state to exactly the point in the past where previous numbers were generated from. From then on, you can call the `predict_*()` methods again to get random numbers, now in the past.

```python
import random, time
from randcrack import RandCrack

random.seed(time.time())

unknown = [random.getrandbits(32) for _ in range(10)]

cracker = RandCrack()

for _ in range(624):
cracker.submit(random.getrandbits(32))

cracker.offset(-624) # Go back -624 states from submitted numbers
cracker.offset(-10) # Go back -10 states to the start of `unknown`

print("Unknown:", unknown)
print("Guesses:", [cracker.predict_getrandbits(32) for _ in range(10)])
```

> **Warning**: The `randint()`, `randrange()` and `choice()` methods all use `randbelow(n)`, which will internally may advance the state **multiple times** depending on the random number that comes from the generator. A number is generated with the number of bits `n` has, but it may still be above `n` the first time. In that case numbers keep being generated in this way until one is below `n`.
>
> This causes predicting **previous** values of these functions to become imprecise as it is not yet known how many numbers were generated with the single function call. You will still be able to generate all the numbers if you offset back further than expected to include all numbers, but there will be an amount of numbers before/after the target sequence (e.g. if the sequence is `[1, 2, 3]`, guesses may be `[123, 42, 1, 2, 3, 1337]`).
>
> This is not a problem with the `getrandbits()` method, as it always does exactly 1. And the `random()` method always does exactly 2
48 changes: 46 additions & 2 deletions randcrack/randcrack.py
Original file line number Diff line number Diff line change
Expand Up @@ -211,6 +211,37 @@ def _regen(self):

self.counter = 0

def untwist(self):
w, n, m = 32, 624, 397
a = 0x9908B0DF

# I like bitshifting more than these custom functions...
MT = [self._to_int(x) for x in self.mt]

for i in range(n-1, -1, -1):
result = 0
tmp = MT[i]
tmp ^= MT[(i + m) % n]
if tmp & (1 << w-1):
tmp ^= a
result = (tmp << 1) & (1 << w-1)
tmp = MT[(i - 1 + n) % n]
tmp ^= MT[(i + m-1) % n]
if tmp & (1 << w-1):
tmp ^= a
result |= 1
result |= (tmp << 1) & ((1 << w-1) - 1)
MT[i] = result

self.mt = [self._to_bitarray(x) for x in MT]

def offset(self, n):
if n >= 0:
[self._predict_32() for _ in range(n)]
else:
[self.untwist() for _ in range(-n // 624 + 1)]
[self._predict_32() for _ in range(624 - (-n % 624))]


if __name__ == "__main__":
import random
Expand All @@ -222,8 +253,21 @@ def _regen(self):

random.seed(time.time())

unknown = [random.getrandbits(32) for _ in range(1000)]

for i in range(624):
cracker.submit(random.randint(0, 4294967294))

print("Guessing next 32000 random bits success rate: {}%"
.format(sum([random.getrandbits(32) == cracker.predict_getrandbits(32) for x in range(1000)]) / 10))
# Future values after syncing
percentage = sum([random.getrandbits(32) == cracker.predict_getrandbits(32) for x in range(1000)]) / 10
print(f"Guessing next 32000 random bits success rate: {percentage}%")
assert percentage == 100

# Previous values
cracker.offset(-1000) # From guessing future
cracker.offset(-624) # From submitting
cracker.offset(-1000) # Back to start of unknown

percentage = sum([unknown[i] == cracker.predict_getrandbits(32) for i in range(1000)]) / 10
print(f"Guessing previous 32000 random bits success rate: {percentage}%")
assert percentage == 100
18 changes: 18 additions & 0 deletions randcrack/test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import random
import time
from randcrack import RandCrack

random.seed(time.time())

unknown = [random.getrandbits(32) for _ in range(10)]

cracker = RandCrack()

for _ in range(624):
cracker.submit(random.getrandbits(32))

cracker.offset(-624) # Go back -624 states from submitted numbers
cracker.offset(-10) # Go back -10 states to the start of `unknown`

print("Unknown:", unknown)
print("Guesses:", [cracker.predict_getrandbits(32) for _ in range(10)])
54 changes: 48 additions & 6 deletions tests/test_randcrack.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ def test_submit_not_enough():

cracker = RandCrack()

for i in range(623):
for _ in range(623):
cracker.submit(random.randint(0, 4294967294))

with pytest.raises(ValueError):
Expand All @@ -23,19 +23,19 @@ def test_submit_too_much():

cracker = RandCrack()

for i in range(624):
for _ in range(624):
cracker.submit(random.randint(0, 4294967294))

with pytest.raises(ValueError):
cracker.submit(random.randint(0, 4294967294))
cracker.submit(random.getrandbits(32))


def test_predict_first_624():
random.seed(time.time())

cracker = RandCrack()

for i in range(624):
for _ in range(624):
cracker.submit(random.randint(0, 4294967294))

assert sum([random.getrandbits(32) == cracker.predict_getrandbits(32) for _ in range(1000)]) == 1000
Expand All @@ -46,16 +46,58 @@ def test_predict_first_1000_close():

cracker = RandCrack()

for i in range(624):
for _ in range(624):
cracker.submit(random.randint(0, 4294967294))

assert sum([random.getrandbits(32) == cracker.predict_getrandbits(32) for _ in range(1000)]) == 1000


def test_predict_random():
random.seed(time.time())

cracker = RandCrack()

for i in range(624):
for _ in range(624):
cracker.submit(random.randint(0, 4294967294))

assert sum([random.random() == cracker.predict_random() for _ in range(1000)]) == 1000


def test_predict_previous():
random.seed(time.time())

unknown = [random.getrandbits(32) for _ in range(1000)]

cracker = RandCrack()

for _ in range(624):
cracker.submit(random.getrandbits(32))

cracker.offset(-624)
cracker.offset(-1000)

assert unknown == [cracker.predict_getrandbits(32) for _ in range(1000)]


def test_predict_previous_randbelow():
random.seed(time.time())

# randint() uses randbelow() internally
unknown = [random.randint(1, 800) for _ in range(1000)]

cracker = RandCrack()

for _ in range(624):
cracker.submit(random.getrandbits(32))

cracker.offset(-624)
cracker.offset(-2000) # Go back too far to include everything

guesses = [cracker.predict_randint(1, 800) for _ in range(2000)]

def is_subseq(x, y):
return all(any(c == ch for c in y) for ch in x)

# The sequence of unknown numbers should be in guesses, but there may be numbers before and after
# (e.g. if the sequence is [1, 2, 3], guesses may be [123, 42, 1, 2, 3, 1337])
assert is_subseq(unknown, guesses)