diff --git a/akka-remote/src/main/scala/akka/remote/security/provider/AES128CounterSecureRNG.scala b/akka-remote/src/main/scala/akka/remote/security/provider/AES128CounterSecureRNG.scala index 108772200e..2fe57d6a09 100644 --- a/akka-remote/src/main/scala/akka/remote/security/provider/AES128CounterSecureRNG.scala +++ b/akka-remote/src/main/scala/akka/remote/security/provider/AES128CounterSecureRNG.scala @@ -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) +} \ No newline at end of file diff --git a/akka-remote/src/main/scala/akka/remote/security/provider/AES256CounterSecureRNG.scala b/akka-remote/src/main/scala/akka/remote/security/provider/AES256CounterSecureRNG.scala index d0d9bc1911..37b2be391d 100644 --- a/akka-remote/src/main/scala/akka/remote/security/provider/AES256CounterSecureRNG.scala +++ b/akka-remote/src/main/scala/akka/remote/security/provider/AES256CounterSecureRNG.scala @@ -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) +} \ No newline at end of file diff --git a/akka-remote/src/main/scala/akka/remote/security/provider/AESCounterBuiltinRNG.scala b/akka-remote/src/main/scala/akka/remote/security/provider/AESCounterBuiltinRNG.scala new file mode 100644 index 0000000000..8e24796db3 --- /dev/null +++ b/akka-remote/src/main/scala/akka/remote/security/provider/AESCounterBuiltinRNG.scala @@ -0,0 +1,131 @@ +/** + * Copyright (C) 2016-2016 Lightbend Inc. + */ +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") +}