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

sin*_*lyr 5 java security random synchronization cryptography

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

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

Maa*_*wes 4

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

一种是简单地采用伪随机数生成器,并且种子显式地使用实例SecureRandom,例如

new SecureRandom(seed);
Run Code Online (Sandbox Code Playgroud)

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

然而,这种方法存在一些问题:

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

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


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

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

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


笔记:

  • 不幸的是,我不知道有任何 Java 的 SecureRandom PRNG 具有良好的确定性和良好的定义。
  • 请注意,也有一些安全方法可以在两个设备上生成共享密钥,例如使用 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);
    }
}
Run Code Online (Sandbox Code Playgroud)

nextBytes在我的 i7 笔记本电脑(平衡电源设置)上,该方法(1 GiB 输入阵列)和计数器模式下的 AES-128需要大约 5 秒,RC4 需要 4 个字节。我在上面使用了 AES-192,否则 XKCD 笑话不起作用。