5

Is there a secure way to create same random numbers on two different devices on java without can predict next number or whole number series by user/coder? I thought a synchronised initiation like firstly users enter same numbers at run of program.This number may be processed with cryptographic techniques(?). And that follows to generate same number series on both of devices. But i really don't know how to do it and how much is it safe?

Note: I have searched for it, but don't have enough knowledge for this specific situation.

sinankzlyr
  • 55
  • 5
  • 3
    Sounds like you want to seed your random number generator identically on both devices. – khelwood Apr 14 '17 at 20:44
  • 1
    If the user/coder can see the stored seed and algorithm, then they can predict the sequence with 100% accuracy. – Elliott Frisch Apr 14 '17 at 20:47
  • Something like TOTP might be what you're looking for. – Lee Daniel Crocker Apr 14 '17 at 20:53
  • The only thing I can think of that's similar is a ["shared secret"](https://en.wikipedia.org/wiki/Shared_secret) between a client and a server. Lots of protocols use these. But the point of a shared secret is that the client can also use it, and you seem to be saying you don't want that. I don't think there's anything really, unless you can describe your use case or design goal better. – markspace Apr 14 '17 at 21:19
  • Yes i want to seed random number generator for both devices. – sinankzlyr Apr 14 '17 at 21:35
  • At least thanks for telling about shared secret . It sounds useful. – sinankzlyr Apr 14 '17 at 21:37
  • @LeeDanielCrocker TOTP looks what i need, thanks – sinankzlyr Apr 15 '17 at 11:23
  • 1
    You should have a look at Diffie-Helman protocol. It solves exactly this problem in a quite elegant way. – ram Apr 15 '17 at 21:27
  • @ram Diffie-Helman looks easiest and safe. – sinankzlyr Apr 16 '17 at 10:32
  • Although the accepted answer has lots of useful stuff (and I totally respect the effort on it), the answer of this question is just this: Diffie-Helman. – ram Apr 16 '17 at 10:37

2 Answers2

4

Basically there are two ways, both require a shared secret of course as markspace already mentioned in the comments.

One is to simply take a pseudo random number generator and seed is explicitly using a SecureRandom instance, e.g.

new SecureRandom(seed);

where seed is a byte array (say, 16 bytes, the size of an AES key) representing the shared secret. As long as you synchronize the calls to the resulting instance then the values should be identical.

There are however a few problems with this approach:

  • the implementation and algorithm may differ between platforms, e.g. IBM and Oracle JDK's or Android for that matter;
  • the implementation and algorithm may differ between different versions of Java;
  • the implementation and algorithm may differ between Java on different runtimes.

Of course you should be able to narrow this down by using a more specific getInstance method, but it's unlikely that this will fully resolve the issue; I'd mostly use it - and I'm currently actually using it - for testing purposes.


Another way is to use the shared secret as input to a stream cipher that produces a key stream. This key stream is generally XOR'ed with the plaintext to form the ciphertext. One of the most easy stream cipher is AES in counter mode (CTR or SIC mode). See below for an implementation that probably includes all the functions you require.

The problem with that approach is that the resulting key stream is just in bits, so you don't get all the goodies of the SecureRandom class, nor the compatibility that it provides. Unfortunately Java doesn't provide Duck typing either (where a class with the same method(s) is considered equal to another type).

The ways around this are two fold: either you implement SecureRandomSpi which handles most of the issues except the seeding. This will require a signing key from Oracle as you cannot use a Service Provider Implementation without creating a signed provider. The other way is to implement SecureRandom directly and override all methods that return random sequences (note that additional methods in future versions of Java would still not be supported). Neither is very appealing.


Notes:

  • Unfortunately I don't know of any SecureRandom PRNG's for Java that are well deterministic and well defined.
  • Note that there are secure ways to generate a shared secret on both devices as well, e.g. using Diffie-Hellman.

OK, I may need this myself, so here comes an optimized (but only limitedly tested) implementation:

package nl.owlstead.stackoverflow;

import java.nio.ByteBuffer;
import java.util.Random;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * A well-defined pseudo-random generator that is based on a stream cipher.
 * <p>
 * This class mimics the {@link Random} class method signatures; it however does currently not provide:
 * <ul>
 * <li>operations returning floats or doubles including returning a Gaussian value in the range [0, 1.0) </li>
 * <li>streams of integers or longs</li>
 * </ul>
 * due to laziness of the developer.
 * It does not allow for re-seeding as re-seeding is not defined for a stream cipher;
 * the same goes from retrieving a seed from the underlying entropy source as it hasn't got one.
 * <p>
 * It is assumed that most significant (leftmost) bytes are taken from the stream cipher first.
 * All the algorithms used to return the random values are well defined, so that compatible implementations can be generated.
 * <p>
 * Instances of this class are stateful and not thread safe.
 * 
 * @author Maarten Bodewes
 */
public class StreamCipherPseudoRandom {

    private static final long TWO_POW_48 = 1L << 48;

    private final Cipher streamCipher;

    // must be a buffer of at least 6 bytes
    // a buffer that is x times 16 is probably most efficient for AES/CTR mode encryption within getBytes(byte[])
    private final ByteBuffer zeros = ByteBuffer.allocate(64);

    /**
     * Creates a SecureRandom from a stream cipher.
     * 
     * @param streamCipher an initialized stream cipher
     * @throws NullPointerException if the cipher is <code>null</code>
     * @throws IllegalStateException if the cipher is not initialized
     * @throws IllegalArgumentException if the cipher is not a stream cipher
     */
    public StreamCipherPseudoRandom(final Cipher streamCipher) {
        if (streamCipher.getOutputSize(1) != 1) {
            throw new IllegalArgumentException("Not a stream cipher");
        }
        this.streamCipher = streamCipher;
    }

    /**
     * Generates a pseudo-random number of bytes by taking exactly the required number of bytes from the stream cipher.
     * 
     * @param data the buffer to be randomized
     */
    public void nextBytes(final byte[] data) {
        generateRandomInBuffer(ByteBuffer.wrap(data));
    }

    /**
     * Generates a pseudo-random boolean value by taking exactly 1 byte from the stream cipher,
     * returning true if and only if the returned value is odd (i.e. if the least significant bit is set to 1), false otherwise.
     * 
     * @return the random boolean
     */
    public boolean nextBoolean() {
        return (generateRandomInBuffer(ByteBuffer.allocate(Byte.BYTES)).get() & 1) == 1;
    }

    /**
     * Generates a pseudo-random <code>int</code> value by taking exactly 4 bytes from the stream cipher.
     * 
     * @return the random <code>int</code> value
     */
    public int nextInt() {
        return generateRandomInBuffer(ByteBuffer.allocate(Integer.BYTES)).getInt();
    }

    /**
     * Generates a pseudo-random <code>long</code> value by taking exactly 8 bytes from the stream cipher.
     * 
     * @return the random <code>long</code> value
     */
    public long nextLong() {
        return generateRandomInBuffer(ByteBuffer.allocate(Long.BYTES)).getLong();
    }

    /**
     * Generates a pseudo-random <code>int</code> value with <code>bits</code> random bits in the lower part of the returned integer.
     * This method takes the minimum number of bytes required to hold the required number of bits from the stream cipher (e.g. 13 bits requires 2 bytes to hold them).
     * 
     * @param bits the number of bits in the integer, between 0 and 32 
     * @return the random <code>int</code> value in the range [0, 2^n) where n is the number of bits
     */
    public int next(final int bits) {
        final int bytes = (bits + Byte.SIZE - 1) / Byte.SIZE;
        final ByteBuffer buf = ByteBuffer.allocate(Integer.BYTES);
        buf.position(Integer.BYTES - bytes);
        generateRandomInBuffer(buf);
        final long l = buf.getInt(0);
        final long m = (1L << bits) - 1;
        return (int) (l & m);
    }

    /**
     * Generates a pseudo-random <code>int</code> value in a range [0, n) by:
     * 
     * <ol>
     * <li>taking 6 bytes from the stream cipher and converting it into a number y</li>
     * <li>restart the procedure if y is larger than x * n where x is the largest value such that x * n <= 2^48
     * <li>return y % n
     * </ol>
     * 
     * An exception to this rule is for n is 1 in which case this method direct returns 0, without taking any bytes from the stream cipher.

     * @param n the maximum value (exclusive) - n must be a non-zero positive number
     * @return the random <code>int</code> value in the range [0, n)
     * @throws IllegalArgumentException if n is zero or negative 
     */
    public int nextInt(final int n) {
        if (n <= 0) {
            throw new IllegalArgumentException("max cannot be negative");
        } else if (n == 1) {
            // only one choice
            return 0;
        }

        final ByteBuffer buf = ByteBuffer.allocate(48 / Byte.SIZE);
        long maxC = TWO_POW_48 - TWO_POW_48 % n;

        long l;
        do {
            buf.clear();
            generateRandomInBuffer(buf);
            // put 16 bits into position 32 to 47
            l = (buf.getShort() & 0xFFFFL) << Integer.SIZE;
            // put 32 bits into position 0 to 31
            l |= buf.getInt() & 0xFFFFFFFFL;
        } while (l > maxC);

       return (int) (l % n);
    }

    /**
     * Retrieves random bytes from the underlying stream cipher.
     * All methods that affect the stream cipher should use this method.
     * The bytes between the position and the limit will contain the random bytes; position and limit are left unchanged.
     * <p>
     * The buffer may not be read only and must support setting a mark; previous marks are discarded.
     * 
     * @param buf the buffer to receive the bytes between the position and limit 
     * @return the same buffer, to allow for 
     */
    protected ByteBuffer generateRandomInBuffer(final ByteBuffer buf) {
        while (buf.hasRemaining()) {
            // clear the zeros buffer
            zeros.clear();
            // set the number of zeros to process
            zeros.limit(Math.min(buf.remaining(), zeros.capacity()));
            try {
                // process the zero's into buf (note that the input size is leading)
                buf.mark();
                streamCipher.update(zeros, buf);
            } catch (ShortBufferException e) {
                // not enough output size, which cannot be true for a stream cipher
                throw new IllegalStateException(
                        String.format("Cipher %s not behaving as a stream cipher", streamCipher.getAlgorithm()));
            }
        }
        buf.reset();
        return buf;
    }

    public static void main(String[] args) throws Exception {
        Cipher streamCipher = Cipher.getInstance("AES/CTR/NoPadding");
        // zero key and iv for demo purposes only
        SecretKey aesKey = new SecretKeySpec(new byte[24], "AES");
        IvParameterSpec iv = new IvParameterSpec(new byte[16]);
        streamCipher.init(Cipher.ENCRYPT_MODE, aesKey, iv);

        StreamCipherPseudoRandom rng = new StreamCipherPseudoRandom(streamCipher);
        // chosen by fair dice roll, guaranteed to be random
        System.out.println(rng.nextInt(6) + 1);
    }
}

Which uses about 5 seconds on my i7 laptop (balanced power setting) for the nextBytes method (1 GiB input array) and AES-128 in counter mode and 4 bytes for RC4. I used AES-192 above otherwise the XKCD joke doesn't work.

Maarten Bodewes
  • 90,524
  • 13
  • 150
  • 263
1

You want a secure RNG. Unfortunately, secure RNGs are designed not to produce the same output on different machines, since they incorporate entropy feeds that are specific to the particular machine.

I suggest you use a AES in ECB mode and encrypt the numbers 0, 1, 2, 3, ... using the same key on both machines. If you don't want 128 bit output (an AES block is 128 bits) then agree to use either the first n bits or the last n bits of the block. Providing both machines stay in step, then each will produce the same pseudo-random output in the same order.

rossum
  • 15,344
  • 1
  • 24
  • 38