Merge pull request #22013 from gosubpl/wip/12636-aes-ctr-prng-alternative-random

AES-CTR with re-seeding (#21740)
This commit is contained in:
Konrad `ktoso` Malawski 2017-01-11 16:09:29 +01:00 committed by GitHub
commit ebc6bc1d50
3 changed files with 162 additions and 32 deletions

View file

@ -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)
private val rng = new AESCounterBuiltinRNG(seed, singleThreadPool)
/**
* Make sure the seed generator is provided by a SecureRandom singleton and not default 'Random'
*/
private val rng = new AESCounterRNG(engineGenerateSeed(Seed128))
/**
* 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)
}

View file

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

View file

@ -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")
}