Skip to content

Add secondary pseudo-random number generator #258

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions PhoenixCrypto/PhoenixCrypto.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
objects = {

/* Begin PBXBuildFile section */
DC35ED842630A381002E441D /* NativeWeakRandom.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC35ED832630A381002E441D /* NativeWeakRandom.swift */; };
DC9B8EC625D6D41400E13818 /* PhoenixCrypto.m in Sources */ = {isa = PBXBuildFile; fileRef = DC9B8EC525D6D41400E13818 /* PhoenixCrypto.m */; };
DC9B8EC725D6D41400E13818 /* PhoenixCrypto.h in CopyFiles */ = {isa = PBXBuildFile; fileRef = DC9B8EC425D6D41400E13818 /* PhoenixCrypto.h */; };
DC9B8ED425D6D7A100E13818 /* NativeChaChaPoly.swift in Sources */ = {isa = PBXBuildFile; fileRef = DC9B8ED325D6D7A100E13818 /* NativeChaChaPoly.swift */; };
Expand All @@ -26,6 +27,7 @@
/* End PBXCopyFilesBuildPhase section */

/* Begin PBXFileReference section */
DC35ED832630A381002E441D /* NativeWeakRandom.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeWeakRandom.swift; sourceTree = "<group>"; };
DC9B8EC125D6D41400E13818 /* libPhoenixCrypto.a */ = {isa = PBXFileReference; explicitFileType = archive.ar; includeInIndex = 0; path = libPhoenixCrypto.a; sourceTree = BUILT_PRODUCTS_DIR; };
DC9B8EC425D6D41400E13818 /* PhoenixCrypto.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = PhoenixCrypto.h; sourceTree = "<group>"; };
DC9B8EC525D6D41400E13818 /* PhoenixCrypto.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = PhoenixCrypto.m; sourceTree = "<group>"; };
Expand Down Expand Up @@ -74,6 +76,7 @@
DC9B8EC425D6D41400E13818 /* PhoenixCrypto.h */,
DC9B8EC525D6D41400E13818 /* PhoenixCrypto.m */,
DC9B8ED325D6D7A100E13818 /* NativeChaChaPoly.swift */,
DC35ED832630A381002E441D /* NativeWeakRandom.swift */,
DC9B8ED225D6D7A000E13818 /* PhoenixCrypto-Bridging-Header.h */,
);
path = Classes;
Expand Down Expand Up @@ -159,6 +162,7 @@
buildActionMask = 2147483647;
files = (
DC9B8ED425D6D7A100E13818 /* NativeChaChaPoly.swift in Sources */,
DC35ED842630A381002E441D /* NativeWeakRandom.swift in Sources */,
DC9B8EC625D6D41400E13818 /* PhoenixCrypto.m in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
8 changes: 2 additions & 6 deletions PhoenixCrypto/PhoenixCrypto/Classes/NativeChaChaPoly.swift
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,16 @@ import Foundation
import CryptoKit
import os.log

//#if DEBUG && true
fileprivate var log = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: "NativeChaChaPoly"
)
//#else
//fileprivate var log = Logger(OSLog.disabled)
//#endif

@objc
public class NativeChaChaPoly: NSObject {

@objc
public class func chachapoly_encrypt(
public class func encrypt(
key: Data,
nonce: Data,
authenticatedData: Data,
Expand Down Expand Up @@ -50,7 +46,7 @@ public class NativeChaChaPoly: NSObject {
}

@objc
public class func chachapoly_decrypt(
public class func decrypt(
key: Data,
nonce: Data,
authenticatedData: Data,
Expand Down
69 changes: 69 additions & 0 deletions PhoenixCrypto/PhoenixCrypto/Classes/NativeWeakRandom.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import Foundation
import CryptoKit
import os.log

fileprivate var log = Logger(
subsystem: Bundle.main.bundleIdentifier!,
category: "NativeWeakRandom"
)

@objc
public class NativeWeakRandom: NSObject {

@objc
private class func toByteArr(i: UInt64) -> [UInt8] {
let count = MemoryLayout<UInt64>.size
var _i = i
return withUnsafePointer(to: &_i) {
$0.withMemoryRebound(to: UInt8.self, capacity: count) {
[UInt8](UnsafeBufferPointer(start: $0, count: count))
}
}
}

@objc
public class func sample() -> Data {

// Sample some entropy from the running process metrics.
let pid = UInt64(getpid())
let usage = rusage_info_current()
var entropy : [UInt8] = toByteArr(i: pid)
entropy += toByteArr(i: usage.ri_user_time)
entropy += toByteArr(i: usage.ri_system_time)
entropy += toByteArr(i: usage.ri_pkg_idle_wkups)
entropy += toByteArr(i: usage.ri_interrupt_wkups)
entropy += toByteArr(i: usage.ri_pageins)
entropy += toByteArr(i: usage.ri_wired_size)
entropy += toByteArr(i: usage.ri_resident_size)
entropy += toByteArr(i: usage.ri_phys_footprint)
entropy += toByteArr(i: usage.ri_proc_start_abstime)
entropy += toByteArr(i: usage.ri_proc_exit_abstime)
entropy += toByteArr(i: usage.ri_child_user_time)
entropy += toByteArr(i: usage.ri_child_system_time)
entropy += toByteArr(i: usage.ri_child_pkg_idle_wkups)
entropy += toByteArr(i: usage.ri_child_interrupt_wkups)
entropy += toByteArr(i: usage.ri_child_pageins)
entropy += toByteArr(i: usage.ri_child_elapsed_abstime)
entropy += toByteArr(i: usage.ri_diskio_bytesread)
entropy += toByteArr(i: usage.ri_diskio_byteswritten)
entropy += toByteArr(i: usage.ri_cpu_time_qos_default)
entropy += toByteArr(i: usage.ri_cpu_time_qos_maintenance)
entropy += toByteArr(i: usage.ri_cpu_time_qos_background)
entropy += toByteArr(i: usage.ri_cpu_time_qos_utility)
entropy += toByteArr(i: usage.ri_cpu_time_qos_legacy)
entropy += toByteArr(i: usage.ri_cpu_time_qos_user_initiated)
entropy += toByteArr(i: usage.ri_cpu_time_qos_user_interactive)
entropy += toByteArr(i: usage.ri_billed_system_time)
entropy += toByteArr(i: usage.ri_serviced_system_time)
entropy += toByteArr(i: usage.ri_logical_writes)
entropy += toByteArr(i: usage.ri_lifetime_max_phys_footprint)
entropy += toByteArr(i: usage.ri_instructions)
entropy += toByteArr(i: usage.ri_cycles)
entropy += toByteArr(i: usage.ri_billed_energy)
entropy += toByteArr(i: usage.ri_serviced_energy)
entropy += toByteArr(i: usage.ri_interval_max_phys_footprint)
entropy += toByteArr(i: usage.ri_runnable_time)

return Data(entropy)
}
}
7 changes: 0 additions & 7 deletions PhoenixCrypto/PhoenixCrypto/Classes/PhoenixCrypto.h
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
//
// PhoenixCrypto.h
// PhoenixCrypto
//
// Created by Robbie Hanson on 2/12/21.
//

#import <Foundation/Foundation.h>
#import "PhoenixCrypto-Swift.h"

Expand Down
7 changes: 0 additions & 7 deletions PhoenixCrypto/PhoenixCrypto/Classes/PhoenixCrypto.m
Original file line number Diff line number Diff line change
@@ -1,10 +1,3 @@
//
// PhoenixCrypto.m
// PhoenixCrypto
//
// Created by Robbie Hanson on 2/12/21.
//

#import "PhoenixCrypto.h"

@implementation PhoenixCrypto
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ package fr.acinq.lightning.crypto
import fr.acinq.bitcoin.*
import fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey
import fr.acinq.bitcoin.DeterministicWallet.hardened
import fr.acinq.lightning.Lightning.secureRandom
import fr.acinq.lightning.channel.ChannelKeys
import fr.acinq.lightning.channel.RecoveredChannelKeys
import fr.acinq.lightning.Lightning.randomLong
import fr.acinq.lightning.transactions.Transactions

data class LocalKeyManager(val seed: ByteVector, val chainHash: ByteVector32) : KeyManager {
Expand Down Expand Up @@ -58,7 +58,7 @@ data class LocalKeyManager(val seed: ByteVector, val chainHash: ByteVector32) :

override fun newFundingKeyPath(isFunder: Boolean): KeyPath {
val last = hardened(if (isFunder) 1 else 0)
fun next() = secureRandom.nextInt().toLong() and 0xFFFFFFFF
fun next() = randomLong() and 0xFFFFFFFF
return KeyPath(listOf(next(), next(), next(), next(), next(), next(), next(), next(), last))
}

Expand Down
54 changes: 54 additions & 0 deletions src/commonMain/kotlin/fr/acinq/lightning/crypto/WeakRandom.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package fr.acinq.lightning.crypto

import fr.acinq.bitcoin.Crypto.sha256
import fr.acinq.bitcoin.crypto.Pack
import fr.acinq.lightning.utils.currentTimestampMillis
import fr.acinq.lightning.utils.runtimeEntropy
import fr.acinq.lightning.utils.xor
import kotlin.native.concurrent.ThreadLocal

/**
* 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.
*/
@ThreadLocal
object WeakRandom {

private var seed = ByteArray(32)
private var stream = ChaCha20(seed, ByteArray(12), 0)
private var lastByte: Byte = 0
private var opsSinceLastSample: Int = 0

private fun sampleEntropy() {
opsSinceLastSample = 0
val commonEntropy = Pack.writeInt64BE(currentTimestampMillis()) + Pack.writeInt32BE(ByteArray(0).hashCode())
val runtimeEntropy = runtimeEntropy()
seed = seed.xor(sha256(commonEntropy + runtimeEntropy))
stream = ChaCha20(seed, ByteArray(12), 0)
}

/** We sample new entropy approximately every 8 operations and at most every 16 operations. */
private fun shouldSample(): Boolean {
opsSinceLastSample += 1
val condition1 = -16 <= lastByte && lastByte <= 16
val condition2 = opsSinceLastSample >= 16
return condition1 || condition2
}

fun nextBytes(array: ByteArray): ByteArray {
if (shouldSample()) {
sampleEntropy()
}

stream.encrypt(array, array, array.size)
lastByte = array.last()

return array
}

fun nextLong(): Long {
val bytes = ByteArray(8)
nextBytes(bytes)
return Pack.int64BE(bytes)
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package fr.acinq.lightning.crypto.noise

import kotlin.random.Random
import fr.acinq.lightning.Lightning.randomBytes

interface DHFunctions {
fun name(): String
Expand Down Expand Up @@ -296,7 +296,7 @@ interface ByteStream {
}

object RandomBytes : ByteStream {
override fun nextBytes(length: Int) = Random.nextBytes(length)
override fun nextBytes(length: Int) = randomBytes(length)
}


Expand Down
14 changes: 11 additions & 3 deletions src/commonMain/kotlin/fr/acinq/lightning/eclair.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,18 +4,22 @@ import fr.acinq.bitcoin.ByteVector32
import fr.acinq.bitcoin.ByteVector64
import fr.acinq.bitcoin.KeyPath
import fr.acinq.bitcoin.PrivateKey
import fr.acinq.lightning.crypto.WeakRandom
import fr.acinq.lightning.utils.secure
import fr.acinq.lightning.utils.xor
import kotlin.experimental.xor
import kotlin.random.Random

object Lightning {

val secureRandom = Random.secure()
private val secureRandom = Random.secure()

fun randomBytes(length: Int): ByteArray {
val buffer = ByteArray(length)
secureRandom.nextBytes(buffer)
return buffer
val weakBuffer = ByteArray(length)
WeakRandom.nextBytes(weakBuffer)
return buffer.xor(weakBuffer)
}

fun randomBytes32(): ByteVector32 = ByteVector32(randomBytes(32))
Expand All @@ -24,10 +28,14 @@ object Lightning {

fun randomKeyPath(length: Int): KeyPath {
val path = mutableListOf<Long>()
repeat(length) { path.add(secureRandom.nextLong()) }
repeat(length) { path.add(randomLong()) }
return KeyPath(path)
}

fun randomLong(): Long {
return secureRandom.nextLong().xor(WeakRandom.nextLong())
}

fun toLongId(fundingTxHash: ByteVector32, fundingOutputIndex: Int): ByteVector32 {
require(fundingOutputIndex < 65536) { "fundingOutputIndex must not be greater than FFFF" }
val x1 = fundingTxHash[30] xor (fundingOutputIndex.shr(8)).toByte()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,9 @@ import kotlin.random.Random

// https://github.com/Kotlin/KEEP/issues/184
expect fun Random.Default.secure(): Random

/**
* This function should return some entropy based on application-specific runtime data.
* 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.
*/
expect fun runtimeEntropy(): ByteArray
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import kotlin.test.assertFailsWith
import kotlin.test.assertTrue


class SchaChainTestsCommon : LightningTestSuite() {
class ShaChainTestsCommon : LightningTestSuite() {
private val expected = listOf(
Hex.decode("f85a0f7f237ed2139855703db4264de380ec731f64a3d832c22a5f2ef615f1d5"),
Hex.decode("a07acb1203f8d7a761eb43e109e46fd877031a6fd2a8e6840f064a49ba826aec"),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package fr.acinq.lightning.crypto

import fr.acinq.bitcoin.crypto.Pack
import fr.acinq.lightning.tests.utils.LightningTestSuite
import fr.acinq.lightning.utils.BitField
import kotlin.math.log2
import kotlin.test.Test
import kotlin.test.assertEquals
import kotlin.test.assertNotSame
import kotlin.test.assertTrue

class WeakRandomTestsCommon : LightningTestSuite() {

@Test
fun `random long generation`() {
val randomNumbers = (1..1000).map { WeakRandom.nextLong() }
assertEquals(1000, randomNumbers.toSet().size)
val entropy = randomNumbers.sumOf { entropyScore(it) } / 1000
assertTrue(entropy >= 0.98)
}

@Test
fun `random bytes generation (small length)`() {
val b1 = ByteArray(32)
WeakRandom.nextBytes(b1)
val b2 = ByteArray(32)
WeakRandom.nextBytes(b2)
val b3 = ByteArray(32)
WeakRandom.nextBytes(b3)
assertNotSame(b1, b2)
assertNotSame(b1, b3)
assertNotSame(b2, b3)
}

@Test
fun `random bytes generation (same length)`() {
var randomBytes = ByteArray(0)
for (i in 1..1000) {
val buffer = ByteArray(64)
WeakRandom.nextBytes(buffer)
randomBytes += buffer
}
val entropy = entropyScore(randomBytes)
assertTrue(entropy >= 0.99)
}

@Test
fun `random bytes generation (variable length)`() {
var randomBytes = ByteArray(0)
for (i in 10..500) {
val buffer = ByteArray(i)
WeakRandom.nextBytes(buffer)
randomBytes += buffer
}
val entropy = entropyScore(randomBytes)
assertTrue(entropy >= 0.99)
}

companion object {
// See https://en.wikipedia.org/wiki/Binary_entropy_function
private fun entropyScore(bits: BitField): Double {
val p = bits.asLeftSequence().fold(0) { acc, bit -> if (bit) acc + 1 else acc }.toDouble() / bits.bitCount
return (-p) * log2(p) - (1 - p) * log2(1 - p)
}

fun entropyScore(l: Long): Double {
return entropyScore(BitField.from(Pack.writeInt64BE(l)))
}

fun entropyScore(bytes: ByteArray): Double {
return entropyScore(BitField.from(bytes))
}
}

}
Loading