Permalink
Browse files

Merge pull request #22013 from gosubpl/wip/12636-aes-ctr-prng-alterna…

…tive-random

AES-CTR with re-seeding (#21740)
  • Loading branch information...
2 parents 1eba865 + 1f20c86 commit ebc6bc1d504728372c37dfb2172a27b86aeb4f56 @ktoso ktoso committed on GitHub Jan 11, 2017
@@ -3,26 +3,26 @@
*/
package akka.remote.security.provider
-import org.uncommons.maths.random.{ AESCounterRNG, SecureRandomSeedGenerator }
+import java.security.SecureRandom
+import java.util.concurrent.Executors
+
import SeedSize.Seed128
+import scala.concurrent.ExecutionContext
+
/**
- * INTERNAL API
- * This class is a wrapper around the 128-bit AESCounterRNG algorithm provided by http://maths.uncommons.org/
+ * This class is a wrapper around the 128-bit AESCounterBuiltinRNG AES/CTR PRNG algorithm
* The only method used by netty ssl is engineNextBytes(bytes)
- * This RNG is good to use to prevent startup delay when you don't have Internet access to random.org
*/
class AES128CounterSecureRNG extends java.security.SecureRandomSpi {
- /**Singleton instance. */
- private final val Instance: SecureRandomSeedGenerator = new SecureRandomSeedGenerator
+ private val singleThreadPool = ExecutionContext.fromExecutorService(Executors.newSingleThreadExecutor(new AESCounterBuiltinRNGReSeeder))
+ private val entropySource = new SecureRandom
+ private val seed = entropySource.generateSeed(Seed128)
- /**
- * Make sure the seed generator is provided by a SecureRandom singleton and not default 'Random'
- */
- private val rng = new AESCounterRNG(engineGenerateSeed(Seed128))
+ private val rng = new AESCounterBuiltinRNG(seed, singleThreadPool)
/**
- * This is managed internally by AESCounterRNG
+ * This is managed internally by AESCounterBuiltinRNG
*/
override protected def engineSetSeed(seed: Array[Byte]): Unit = ()
@@ -34,13 +34,11 @@ class AES128CounterSecureRNG extends java.security.SecureRandomSpi {
override protected def engineNextBytes(bytes: Array[Byte]): Unit = rng.nextBytes(bytes)
/**
- * Unused method
- * Returns the given number of seed bytes. This call may be used to
- * seed other random number generators.
+ * For completeness of SecureRandomSpi API implementation
+ * Returns the given number of seed bytes.
*
* @param numBytes the number of seed bytes to generate.
* @return the seed bytes.
*/
- override protected def engineGenerateSeed(numBytes: Int): Array[Byte] = Instance.generateSeed(numBytes)
-}
-
+ override protected def engineGenerateSeed(numBytes: Int): Array[Byte] = entropySource.generateSeed(numBytes)
+}
@@ -3,23 +3,26 @@
*/
package akka.remote.security.provider
-import org.uncommons.maths.random.{ AESCounterRNG, SecureRandomSeedGenerator }
+import java.security.SecureRandom
+import java.util.concurrent.Executors
+
import SeedSize.Seed256
+import scala.concurrent.ExecutionContext
+
/**
- * INTERNAL API
- * This class is a wrapper around the 256-bit AESCounterRNG algorithm provided by http://maths.uncommons.org/
+ * This class is a wrapper around the 256-bit AESCounterBuiltinRNG AES/CTR PRNG algorithm
* The only method used by netty ssl is engineNextBytes(bytes)
- * This RNG is good to use to prevent startup delay when you don't have Internet access to random.org
*/
class AES256CounterSecureRNG extends java.security.SecureRandomSpi {
- /**Singleton instance. */
- private final val Instance: SecureRandomSeedGenerator = new SecureRandomSeedGenerator
+ private val singleThreadPool = ExecutionContext.fromExecutorService(Executors.newSingleThreadExecutor(new AESCounterBuiltinRNGReSeeder))
+ private val entropySource = new SecureRandom
+ private val seed = entropySource.generateSeed(Seed256)
- private val rng = new AESCounterRNG(engineGenerateSeed(Seed256))
+ private val rng = new AESCounterBuiltinRNG(seed, singleThreadPool)
/**
- * This is managed internally by AESCounterRNG
+ * This is managed internally by AESCounterBuiltinRNG
*/
override protected def engineSetSeed(seed: Array[Byte]): Unit = ()
@@ -31,13 +34,11 @@ class AES256CounterSecureRNG extends java.security.SecureRandomSpi {
override protected def engineNextBytes(bytes: Array[Byte]): Unit = rng.nextBytes(bytes)
/**
- * Unused method
- * Returns the given number of seed bytes. This call may be used to
- * seed other random number generators.
+ * For completeness of SecureRandomSpi API implementation
+ * Returns the given number of seed bytes.
*
* @param numBytes the number of seed bytes to generate.
* @return the seed bytes.
*/
- override protected def engineGenerateSeed(numBytes: Int): Array[Byte] = Instance.generateSeed(numBytes)
-}
-
+ override protected def engineGenerateSeed(numBytes: Int): Array[Byte] = entropySource.generateSeed(numBytes)
+}
@@ -0,0 +1,131 @@
+/**
+ * Copyright (C) 2016-2016 Lightbend Inc. <http://www.lightbend.com>
+ */
+package akka.remote.security.provider
+
+import java.security.{ Key, SecureRandom }
+import java.util.Random
+import java.util.concurrent.ThreadFactory
+import javax.crypto.Cipher
+import javax.crypto.spec.IvParameterSpec
+
+import scala.concurrent.duration.{ Duration, FiniteDuration }
+import scala.concurrent.{ Await, ExecutionContext, Future, duration }
+
+/**
+ * INTERNAL API
+ * This class is a Scala implementation of AESCounterRNG algorithm
+ * patterned after org.uncommons.maths.random by Daniel Dyer (Apache License 2.0)
+ *
+ * Non-linear random number generator based on the AES block cipher in counter mode.
+ * Uses the seed as a key to encrypt a 128-bit counter using AES(Rijndael).
+ *
+ * Keys larger than 128-bit for the AES cipher require
+ * the inconvenience of installing the unlimited strength cryptography policy
+ * files for the Java platform. Larger keys may be used (192 or 256 bits) but if the
+ * cryptography policy files are not installed, a
+ * java.security.GeneralSecurityException will be thrown.
+ *
+ * NOTE: this class is not serializable
+ */
+// FIMXE add @InternalApi
+private[akka] class AESCounterBuiltinRNG(val seed: Array[Byte], implicit val executionContext: ExecutionContext,
+ val reseedingThreshold: Long = CounterRNGConstants.ReseedingThreshold,
+ val reseedingDeadline: Long = CounterRNGConstants.ReseedingDeadline,
+ val reseedingTimeout: Duration = CounterRNGConstants.ReseedingTimeout) extends Random {
+ import CounterRNGConstants._
+
+ private val entropySource = new SecureRandom
+
+ // mutable state below, concurrent accesses need synchronized or lock
+ private val counter: Array[Byte] = Array.fill[Byte](CounterSizeBytes)(0)
+ private var index: Int = 0
+ private var currentBlock: Array[Byte] = null
+ private var reseedFuture: Future[Array[Byte]] = null
+ private var bitsSinceSeeding: Long = 0
+
+ private val cipher = Cipher.getInstance("AES/CTR/NoPadding")
+ private val ivArr = Array.fill[Byte](CounterSizeBytes)(0)
+ ivArr(0) = (ivArr(0) + 1.toByte).toByte
+ private val ivSpec = new IvParameterSpec(ivArr)
+ cipher.init(Cipher.ENCRYPT_MODE, new this.AESKey(seed), ivSpec)
+
+ @Override
+ override protected def next(bits: Int): Int = synchronized {
+ // random result generation phase - if there is not enough bits in counter variable
+ // we generate some more with AES/CTR
+ bitsSinceSeeding += bits
+ if (currentBlock == null || currentBlock.length - index < 4) {
+ try {
+ currentBlock = cipher.doFinal(counter)
+ index = 0
+ } catch {
+ case ex: Exception
+ // Generally Cipher.doFinal() from nextBlock may throw various exceptions.
+ // However this should never happen. If initialisation succeeds without exceptions
+ // we should be able to proceed indefinitely without exceptions.
+ throw new IllegalStateException("Failed creating next random block.", ex)
+ }
+ }
+
+ // now, enough bits in counter, generate pseudo-random result
+ val result = (BitwiseByteToInt & currentBlock(index + 3)) |
+ ((BitwiseByteToInt & currentBlock(index + 2)) << 8) |
+ ((BitwiseByteToInt & currentBlock(index + 1)) << 16) |
+ ((BitwiseByteToInt & currentBlock(index)) << 24)
+
+ // re-seeding phase
+ // first, we check if reseedingThreshold is exceeded to see if new entropy is required
+ // we can still proceed without it, but we should ask for it
+ if (bitsSinceSeeding > reseedingThreshold) {
+ if (reseedFuture == null) {
+ // ask for a seed and process async on a separate thread using AESCounterBuiltinRNGReSeeder threadpool
+ reseedFuture = Future { entropySource.generateSeed(32) }
+ }
+ // check if reseedingDeadline is exceeded - in that case we cannot proceed, as that would be insecure
+ // we need to block on the future to wait for entropy
+ if (bitsSinceSeeding > reseedingDeadline) {
+ try {
+ Await.ready(reseedFuture, reseedingTimeout)
+ } catch {
+ case ex: Exception
+ Console.err.println(s"[ERROR] AESCounterBuiltinRNG re-seeding failed or timed out after ${reseedingTimeout.toSeconds.toString}s !")
+ }
+ }
+ // check if future has completed and retrieve additional entropy if that is the case
+ if (reseedFuture != null && reseedFuture.isCompleted) {
+ if (reseedFuture.value.get.isSuccess) { // we have re-seeded with success
+ val newSeed = reseedFuture.value.get.get // this is safe
+ cipher.init(Cipher.ENCRYPT_MODE, new this.AESKey(newSeed), ivSpec)
+ currentBlock = null
+ bitsSinceSeeding = 0 // reset re-seeding counter
+ }
+ reseedFuture = null // request creation of new seed when needed
+ }
+ }
+
+ index += 4
+ result >>> (32 - bits)
+ }
+
+ /**
+ * Trivial key implementation for use with AES cipher.
+ */
+ final private class AESKey(val keyData: Array[Byte]) extends Key {
+ def getAlgorithm: String = "AES"
+ def getFormat: String = "RAW"
+ def getEncoded: Array[Byte] = keyData
+ }
+}
+
+private object CounterRNGConstants {
+ final val CounterSizeBytes = 16
+ final val BitwiseByteToInt = 0x000000FF
+ final val ReseedingThreshold = 1000000000L // threshold for requesting new entropy (should give us ample time to re-seed)
+ final val ReseedingDeadline = 140737488355328L // deadline for obtaining new entropy 2^47 safe as per SP800-90
+ final val ReseedingTimeout: FiniteDuration = Duration.apply(5, duration.MINUTES) // timeout for re-seeding (on Linux read from /dev/random)
+}
+
+private class AESCounterBuiltinRNGReSeeder extends ThreadFactory {
+ override def newThread(r: Runnable): Thread = new Thread(r, "AESCounterBuiltinRNGReSeeder")
+}

0 comments on commit ebc6bc1

Please sign in to comment.