-
Notifications
You must be signed in to change notification settings - Fork 265
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
In case of catastrophic failures of the `SecureRandom` instance, we add a secondary randomness source that we mix into the random stream. This is a somewhat weak random source and should not be used on its own, but it doesn't hurt to xor it with the output of `SecureRandom`. We use an actor that listens to events in the system and inject them in our weak pseudo-RNG.
- Loading branch information
Showing
78 changed files
with
1,114 additions
and
730 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
172 changes: 172 additions & 0 deletions
172
eclair-core/src/main/scala/fr/acinq/eclair/crypto/Random.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,172 @@ | ||
/* | ||
* Copyright 2021 ACINQ SAS | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package fr.acinq.eclair.crypto | ||
|
||
import fr.acinq.bitcoin.Protocol | ||
import org.bouncycastle.crypto.digests.SHA256Digest | ||
import org.bouncycastle.crypto.engines.ChaCha7539Engine | ||
import org.bouncycastle.crypto.params.{KeyParameter, ParametersWithIV} | ||
|
||
import java.lang.management.ManagementFactory | ||
import java.nio.ByteOrder | ||
import java.security.SecureRandom | ||
|
||
/** | ||
* Created by t-bast on 19/04/2021. | ||
*/ | ||
|
||
sealed trait EntropyCollector { | ||
/** External components may inject additional entropy to be added to the entropy pool. */ | ||
def addEntropy(entropy: Array[Byte]): Unit | ||
} | ||
|
||
sealed trait RandomGenerator { | ||
// @formatter:off | ||
def nextBytes(bytes: Array[Byte]): Unit | ||
def nextLong(): Long | ||
// @formatter:on | ||
} | ||
|
||
sealed trait RandomGeneratorWithInit extends RandomGenerator { | ||
def init(): Unit | ||
} | ||
|
||
/** | ||
* A weak pseudo-random number generator that regularly samples a few entropy sources to build a hash chain. | ||
* This should never be used alone but can be xor-ed with the OS random number generator in case it completely breaks. | ||
*/ | ||
private class WeakRandom() extends RandomGenerator { | ||
|
||
private val stream = new ChaCha7539Engine() | ||
private val seed = new Array[Byte](32) | ||
private var lastByte: Byte = 0 | ||
private var opsSinceLastSample: Int = 0 | ||
|
||
private val memoryMXBean = ManagementFactory.getMemoryMXBean | ||
private val runtimeMXBean = ManagementFactory.getRuntimeMXBean | ||
private val threadMXBean = ManagementFactory.getThreadMXBean | ||
|
||
// sample some initial entropy | ||
sampleEntropy() | ||
|
||
private def feedDigest(sha: SHA256Digest, i: Int): Unit = { | ||
sha.update(i.toByte) | ||
sha.update((i >> 8).toByte) | ||
sha.update((i >> 16).toByte) | ||
sha.update((i >> 24).toByte) | ||
} | ||
|
||
private def feedDigest(sha: SHA256Digest, l: Long): Unit = { | ||
sha.update(l.toByte) | ||
sha.update((l >> 8).toByte) | ||
sha.update((l >> 16).toByte) | ||
sha.update((l >> 24).toByte) | ||
sha.update((l >> 32).toByte) | ||
sha.update((l >> 40).toByte) | ||
} | ||
|
||
/** The entropy pool is regularly enriched with newly sampled entropy. */ | ||
private def sampleEntropy(): Unit = { | ||
opsSinceLastSample = 0 | ||
|
||
val sha = new SHA256Digest() | ||
sha.update(seed, 0, 32) | ||
feedDigest(sha, System.currentTimeMillis()) | ||
feedDigest(sha, System.identityHashCode(new Array[Int](1))) | ||
feedDigest(sha, memoryMXBean.getHeapMemoryUsage.getUsed) | ||
feedDigest(sha, memoryMXBean.getNonHeapMemoryUsage.getUsed) | ||
feedDigest(sha, runtimeMXBean.getPid) | ||
feedDigest(sha, runtimeMXBean.getUptime) | ||
feedDigest(sha, threadMXBean.getCurrentThreadCpuTime) | ||
feedDigest(sha, threadMXBean.getCurrentThreadUserTime) | ||
feedDigest(sha, threadMXBean.getPeakThreadCount) | ||
|
||
sha.doFinal(seed, 0) | ||
// NB: init internally resets the engine, no need to reset it explicitly ourselves. | ||
stream.init(true, new ParametersWithIV(new KeyParameter(seed), new Array[Byte](12))) | ||
} | ||
|
||
/** We sample new entropy approximately every 32 operations and at most every 64 operations. */ | ||
private def shouldSample(): Boolean = { | ||
opsSinceLastSample += 1 | ||
val condition1 = -4 <= lastByte && lastByte <= 4 | ||
val condition2 = opsSinceLastSample >= 64 | ||
condition1 || condition2 | ||
} | ||
|
||
def addEntropy(entropy: Array[Byte]): Unit = synchronized { | ||
if (entropy.nonEmpty) { | ||
val sha = new SHA256Digest() | ||
sha.update(seed, 0, 32) | ||
sha.update(entropy, 0, entropy.length) | ||
sha.doFinal(seed, 0) | ||
// NB: init internally resets the engine, no need to reset it explicitly ourselves. | ||
stream.init(true, new ParametersWithIV(new KeyParameter(seed), new Array[Byte](12))) | ||
} | ||
} | ||
|
||
def nextBytes(bytes: Array[Byte]): Unit = synchronized { | ||
if (shouldSample()) { | ||
sampleEntropy() | ||
} | ||
stream.processBytes(bytes, 0, bytes.length, bytes, 0) | ||
lastByte = bytes.last | ||
} | ||
|
||
def nextLong(): Long = { | ||
val bytes = new Array[Byte](8) | ||
nextBytes(bytes) | ||
Protocol.uint64(bytes, ByteOrder.BIG_ENDIAN) | ||
} | ||
|
||
} | ||
|
||
class StrongRandom() extends RandomGeneratorWithInit with EntropyCollector { | ||
|
||
/** | ||
* We are using 'new SecureRandom()' instead of 'SecureRandom.getInstanceStrong()' because the latter can hang on Linux | ||
* See http://bugs.java.com/view_bug.do?bug_id=6521844 and https://tersesystems.com/2015/12/17/the-right-way-to-use-securerandom/ | ||
*/ | ||
private val secureRandom = new SecureRandom() | ||
|
||
/** | ||
* We're using an additional, weaker randomness source to protect against catastrophic failures of the SecureRandom | ||
* instance. | ||
*/ | ||
private val weakRandom = new WeakRandom() | ||
|
||
override def init(): Unit = { | ||
// this will force the secure random instance to initialize itself right now, making sure it doesn't hang later | ||
secureRandom.nextInt() | ||
} | ||
|
||
override def addEntropy(entropy: Array[Byte]): Unit = { | ||
weakRandom.addEntropy(entropy) | ||
} | ||
|
||
override def nextBytes(bytes: Array[Byte]): Unit = { | ||
secureRandom.nextBytes(bytes) | ||
val buffer = new Array[Byte](bytes.length) | ||
weakRandom.nextBytes(buffer) | ||
for (i <- bytes.indices) { | ||
bytes(i) = (bytes(i) ^ buffer(i)).toByte | ||
} | ||
} | ||
|
||
override def nextLong(): Long = secureRandom.nextLong() ^ weakRandom.nextLong() | ||
|
||
} |
97 changes: 97 additions & 0 deletions
97
eclair-core/src/main/scala/fr/acinq/eclair/crypto/WeakEntropyPool.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,97 @@ | ||
/* | ||
* Copyright 2021 ACINQ SAS | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License. | ||
*/ | ||
|
||
package fr.acinq.eclair.crypto | ||
|
||
import akka.actor.typed.Behavior | ||
import akka.actor.typed.eventstream.EventStream | ||
import akka.actor.typed.scaladsl.Behaviors | ||
import fr.acinq.bitcoin.Crypto.PublicKey | ||
import fr.acinq.bitcoin.{Block, ByteVector32, ByteVector64, Crypto} | ||
import fr.acinq.eclair.blockchain.NewBlock | ||
import fr.acinq.eclair.channel.ChannelSignatureReceived | ||
import fr.acinq.eclair.io.PeerConnected | ||
import fr.acinq.eclair.payment.ChannelPaymentRelayed | ||
import fr.acinq.eclair.router.NodeUpdated | ||
import scodec.bits.ByteVector | ||
|
||
import scala.concurrent.duration.DurationInt | ||
|
||
/** | ||
* Created by t-bast on 20/04/2021. | ||
*/ | ||
|
||
/** | ||
* This actor gathers entropy from several events and from the runtime, and regularly injects it into our [[WeakRandom]] | ||
* instance. | ||
* | ||
* Note that this isn't a strong entropy pool and shouldn't be trusted on its own but rather used as a safeguard against | ||
* failures in [[java.security.SecureRandom]]. | ||
*/ | ||
object WeakEntropyPool { | ||
|
||
// @formatter:off | ||
sealed trait Command | ||
private case object FlushEntropy extends Command | ||
private case class WrappedNewBlock(block: Block) extends Command | ||
private case class WrappedPaymentRelayed(paymentHash: ByteVector32, relayedAt: Long) extends Command | ||
private case class WrappedPeerConnected(nodeId: PublicKey) extends Command | ||
private case class WrappedChannelSignature(wtxid: ByteVector32) extends Command | ||
private case class WrappedNodeUpdated(sig: ByteVector64) extends Command | ||
// @formatter:on | ||
|
||
def apply(collector: EntropyCollector): Behavior[Command] = { | ||
Behaviors.setup { context => | ||
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[NewBlock](e => WrappedNewBlock(e.block))) | ||
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelPaymentRelayed](e => WrappedPaymentRelayed(e.paymentHash, e.timestamp))) | ||
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[PeerConnected](e => WrappedPeerConnected(e.nodeId))) | ||
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[NodeUpdated](e => WrappedNodeUpdated(e.ann.signature))) | ||
context.system.eventStream ! EventStream.Subscribe(context.messageAdapter[ChannelSignatureReceived](e => WrappedChannelSignature(e.commitments.localCommit.publishableTxs.commitTx.tx.wtxid))) | ||
Behaviors.withTimers { timers => | ||
timers.startTimerWithFixedDelay(FlushEntropy, 30 seconds) | ||
collecting(collector, None) | ||
} | ||
} | ||
} | ||
|
||
private def collecting(collector: EntropyCollector, entropy_opt: Option[ByteVector32]): Behavior[Command] = { | ||
Behaviors.receiveMessage { | ||
case FlushEntropy => | ||
entropy_opt match { | ||
case Some(entropy) => | ||
collector.addEntropy(entropy.toArray) | ||
collecting(collector, None) | ||
case None => | ||
Behaviors.same | ||
} | ||
|
||
case WrappedNewBlock(block) => collecting(collector, collect(entropy_opt, block.hash ++ ByteVector.fromLong(System.currentTimeMillis()))) | ||
|
||
case WrappedPaymentRelayed(paymentHash, relayedAt) => collecting(collector, collect(entropy_opt, paymentHash ++ ByteVector.fromLong(relayedAt))) | ||
|
||
case WrappedPeerConnected(nodeId) => collecting(collector, collect(entropy_opt, nodeId.value ++ ByteVector.fromLong(System.currentTimeMillis()))) | ||
|
||
case WrappedNodeUpdated(sig) => collecting(collector, collect(entropy_opt, sig ++ ByteVector.fromLong(System.currentTimeMillis()))) | ||
|
||
case WrappedChannelSignature(wtxid) => collecting(collector, collect(entropy_opt, wtxid ++ ByteVector.fromLong(System.currentTimeMillis()))) | ||
} | ||
} | ||
|
||
private def collect(entropy_opt: Option[ByteVector32], additional: ByteVector): Option[ByteVector32] = { | ||
Some(Crypto.sha256(entropy_opt.map(_.bytes).getOrElse(ByteVector.empty) ++ additional)) | ||
} | ||
|
||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.