是否可以在两个不同的设备上创建同步随机数?

时间:2017-04-14 20:42:23

标签: java security random cryptography synchronization

是否有一种安全的方法可以在java上的两个不同设备上创建相同的随机数而无法通过用户/编码器预测下一个数字或整数系列? 我认为同步启动就像首先用户在程序运行时输入相同的数字一样。这个数字可以用加密技术处理(?)。接下来在两个设备上生成相同的数字序列。但我真的不知道该怎么做以及它有多安全?

注意:我已经搜索过了,但对这种特定情况没有足够的知识。

2 个答案:

答案 0 :(得分:3)

基本上有两种方式,当然需要共享秘密作为评论中已经提到的标记空间。

一种是简单地使用伪随机数生成器,种子明确使用SecureRandom实例,例如

new SecureRandom(seed);

其中seed是表示共享密钥的字节数组(例如,16字节,AES密钥的大小)。只要您将调用同步到结果实例,那么值应该相同。

这种方法存在一些问题:

  • 平台之间的实现和算法可能不同,例如就此而言,IBM和Oracle JDK或Android;
  • 不同版本的Java之间的实现和算法可能不同;
  • 不同运行时的Java实现和算法可能不同。

当然,您应该可以使用更具体的getInstance方法缩小范围,但这不太可能完全解决问题;我主要使用它 - 我目前正在使用它 - 用于测试目的。

另一种方法是使用共享密钥作为生成密钥流流密码的输入。该密钥流通常与明文进行异或,以形成密文。最简单的流密码之一是计数器模式下的AES(CTR或SIC模式)。 请参阅下面的实施,其中可能包含您需要的所有功能

该方法的问题在于生成的密钥流只是位,因此您不能获得SecureRandom类的所有好处,也不能获得它提供的兼容性。不幸的是,Java也没有提供Duck类型(其中具有相同方法的类被认为与另一种类型相同)。

解决这个问题的方法有两个方面:要么实施SecureRandomSpi来处理除了播种以外的大多数问题。这将需要来自Oracle的签名密钥,因为您无法使用服务提供商实施而无需创建签名提供商。另一种方法是直接实现SecureRandom并覆盖返回随机序列的所有方法(请注意,仍然不支持将来Java版本中的其他方法)。两者都没有吸引力。

注意:

  • 不幸的是,我不知道任何确定性和定义良好的SecureRandom PRNG用于Java。
  • 请注意,还有安全的方法可以在两台设备上生成共享密钥,例如:使用Diffie-Hellman。

好的,我自己可能需要这个,所以这里有一个优化的(但只是经过有限测试的)实现:

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

我的i7笔记本电脑(平衡功率设置)使用约5秒钟用于nextBytes方法(1 GiB输入数组)和计数器模式下的AES-128以及RC4的4字节。我使用AES-192,否则XKCD的笑话不起作用。

答案 1 :(得分:1)

您想要一个安全的RNG。不幸的是,安全的RNG被设计为不在不同的机器上产生相同的输出,因为它们包含特定于特定机器的熵馈送。

我建议你在ECB模式下使用AES,并在两台机器上使用相同的密钥加密数字0,1,2,3 .......如果您不想要128位输出(AES块是128位),则同意使用块的前n位或后n位。提供两台机器保持步调,然后每个机器将以相同的顺序产生相同的伪随机输出。