Skip to content

Commit f03a3c9

Browse files
committed
Add WeakRandom prng
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. The `runtimeEntropy` should be customized for iOS and Android.
1 parent f0fee2e commit f03a3c9

File tree

7 files changed

+157
-4
lines changed

7 files changed

+157
-4
lines changed
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package fr.acinq.lightning.crypto
2+
3+
import fr.acinq.bitcoin.Crypto.sha256
4+
import fr.acinq.bitcoin.crypto.Pack
5+
import fr.acinq.lightning.utils.currentTimestampMillis
6+
import fr.acinq.lightning.utils.runtimeEntropy
7+
import fr.acinq.lightning.utils.xor
8+
import kotlin.native.concurrent.ThreadLocal
9+
10+
/**
11+
* A weak pseudo-random number generator that regularly samples a few entropy sources to build a hash chain.
12+
* This should never be used alone but can be xor-ed with the OS random number generator in case it completely breaks.
13+
*/
14+
@ThreadLocal
15+
object WeakRandom {
16+
17+
private var seed = ByteArray(32)
18+
private var stream = ChaCha20(seed, ByteArray(12), 0)
19+
private var lastByte: Byte = 0
20+
private var opsSinceLastSample: Int = 0
21+
22+
private fun sampleEntropy() {
23+
opsSinceLastSample = 0
24+
val commonEntropy = Pack.writeInt64BE(currentTimestampMillis()) + Pack.writeInt32BE(ByteArray(0).hashCode())
25+
val runtimeEntropy = runtimeEntropy()
26+
seed = seed.xor(sha256(commonEntropy + runtimeEntropy))
27+
stream = ChaCha20(seed, ByteArray(12), 0)
28+
}
29+
30+
/** We sample new entropy approximately every 8 operations and at most every 16 operations. */
31+
private fun shouldSample(): Boolean {
32+
opsSinceLastSample += 1
33+
val condition1 = -16 <= lastByte && lastByte <= 16
34+
val condition2 = opsSinceLastSample >= 16
35+
return condition1 || condition2
36+
}
37+
38+
fun nextBytes(array: ByteArray): ByteArray {
39+
if (shouldSample()) {
40+
sampleEntropy()
41+
}
42+
43+
stream.encrypt(array, array, array.size)
44+
lastByte = array.last()
45+
46+
return array
47+
}
48+
49+
fun nextLong(): Long {
50+
val bytes = ByteArray(8)
51+
nextBytes(bytes)
52+
return Pack.int64BE(bytes)
53+
}
54+
}

src/commonMain/kotlin/fr/acinq/lightning/crypto/noise/Noise.kt

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
package fr.acinq.lightning.crypto.noise
22

3-
import kotlin.random.Random
3+
import fr.acinq.lightning.Lightning.randomBytes
44

55
interface DHFunctions {
66
fun name(): String
@@ -296,7 +296,7 @@ interface ByteStream {
296296
}
297297

298298
object RandomBytes : ByteStream {
299-
override fun nextBytes(length: Int) = Random.nextBytes(length)
299+
override fun nextBytes(length: Int) = randomBytes(length)
300300
}
301301

302302

src/commonMain/kotlin/fr/acinq/lightning/eclair.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,9 @@ import fr.acinq.bitcoin.ByteVector32
44
import fr.acinq.bitcoin.ByteVector64
55
import fr.acinq.bitcoin.KeyPath
66
import fr.acinq.bitcoin.PrivateKey
7+
import fr.acinq.lightning.crypto.WeakRandom
78
import fr.acinq.lightning.utils.secure
9+
import fr.acinq.lightning.utils.xor
810
import kotlin.experimental.xor
911
import kotlin.random.Random
1012

@@ -15,7 +17,9 @@ object Lightning {
1517
fun randomBytes(length: Int): ByteArray {
1618
val buffer = ByteArray(length)
1719
secureRandom.nextBytes(buffer)
18-
return buffer
20+
val weakBuffer = ByteArray(length)
21+
WeakRandom.nextBytes(weakBuffer)
22+
return buffer.xor(weakBuffer)
1923
}
2024

2125
fun randomBytes32(): ByteVector32 = ByteVector32(randomBytes(32))
@@ -29,7 +33,7 @@ object Lightning {
2933
}
3034

3135
fun randomLong(): Long {
32-
return secureRandom.nextLong()
36+
return secureRandom.nextLong().xor(WeakRandom.nextLong())
3337
}
3438

3539
fun toLongId(fundingTxHash: ByteVector32, fundingOutputIndex: Int): ByteVector32 {

src/commonMain/kotlin/fr/acinq/lightning/utils/SecureRandom.kt

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,9 @@ import kotlin.random.Random
44

55
// https://github.com/Kotlin/KEEP/issues/184
66
expect fun Random.Default.secure(): Random
7+
8+
/**
9+
* This function should return some entropy based on application-specific runtime data.
10+
* It doesn't need to be very strong entropy as it's only used as a backup, but it should be as good as possible.
11+
*/
12+
expect fun runtimeEntropy(): ByteArray
Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package fr.acinq.lightning.crypto
2+
3+
import fr.acinq.bitcoin.crypto.Pack
4+
import fr.acinq.lightning.tests.utils.LightningTestSuite
5+
import fr.acinq.lightning.utils.BitField
6+
import kotlin.math.log2
7+
import kotlin.test.Test
8+
import kotlin.test.assertEquals
9+
import kotlin.test.assertNotSame
10+
import kotlin.test.assertTrue
11+
12+
class WeakRandomTestsCommon : LightningTestSuite() {
13+
14+
@Test
15+
fun `random long generation`() {
16+
val randomNumbers = (1..1000).map { WeakRandom.nextLong() }
17+
assertEquals(1000, randomNumbers.toSet().size)
18+
val entropy = randomNumbers.sumOf { entropyScore(it) } / 1000
19+
assertTrue(entropy >= 0.98)
20+
}
21+
22+
@Test
23+
fun `random bytes generation (small length)`() {
24+
val b1 = ByteArray(32)
25+
WeakRandom.nextBytes(b1)
26+
val b2 = ByteArray(32)
27+
WeakRandom.nextBytes(b2)
28+
val b3 = ByteArray(32)
29+
WeakRandom.nextBytes(b3)
30+
assertNotSame(b1, b2)
31+
assertNotSame(b1, b3)
32+
assertNotSame(b2, b3)
33+
}
34+
35+
@Test
36+
fun `random bytes generation (same length)`() {
37+
var randomBytes = ByteArray(0)
38+
for (i in 1..1000) {
39+
val buffer = ByteArray(64)
40+
WeakRandom.nextBytes(buffer)
41+
randomBytes += buffer
42+
}
43+
val entropy = entropyScore(randomBytes)
44+
assertTrue(entropy >= 0.99)
45+
}
46+
47+
@Test
48+
fun `random bytes generation (variable length)`() {
49+
var randomBytes = ByteArray(0)
50+
for (i in 10..500) {
51+
val buffer = ByteArray(i)
52+
WeakRandom.nextBytes(buffer)
53+
randomBytes += buffer
54+
}
55+
val entropy = entropyScore(randomBytes)
56+
assertTrue(entropy >= 0.99)
57+
}
58+
59+
companion object {
60+
// See https://en.wikipedia.org/wiki/Binary_entropy_function
61+
private fun entropyScore(bits: BitField): Double {
62+
val p = bits.asLeftSequence().fold(0) { acc, bit -> if (bit) acc + 1 else acc }.toDouble() / bits.bitCount
63+
return (-p) * log2(p) - (1 - p) * log2(1 - p)
64+
}
65+
66+
fun entropyScore(l: Long): Double {
67+
return entropyScore(BitField.from(Pack.writeInt64BE(l)))
68+
}
69+
70+
fun entropyScore(bytes: ByteArray): Double {
71+
return entropyScore(BitField.from(bytes))
72+
}
73+
}
74+
75+
}

src/jvmMain/kotlin/fr/acinq/lightning/utils/SecureRandomJvm.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,13 @@ class SecureRandomJvm : Random() {
2828
}
2929

3030
actual fun Random.Default.secure(): Random = SecureRandomJvm()
31+
32+
actual fun runtimeEntropy(): ByteArray {
33+
val freeMemory = Runtime.getRuntime().freeMemory()
34+
val b = ByteArray(4)
35+
b.set(0, freeMemory.toByte())
36+
b.set(1, (freeMemory shr 8).toByte())
37+
b.set(2, (freeMemory shr 16).toByte())
38+
b.set(3, (freeMemory shr 24).toByte())
39+
return ByteArray(0)
40+
}

src/nativeMain/kotlin/fr/acinq/lightning/utils/SecureRandomPosix.kt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,3 +47,7 @@ object SecureRandomPosix : Random() {
4747
}
4848

4949
actual fun Random.Default.secure(): Random = SecureRandomPosix
50+
51+
actual fun runtimeEntropy(): ByteArray {
52+
return ByteArray(32)
53+
}

0 commit comments

Comments
 (0)