Skip to content

Commit

Permalink
reimplement linux random:
Browse files Browse the repository at this point in the history
* support getrandom sys call
* use getrandom by default with fallback to urandom
* cache file descriptor for urandom in ThreadLocal
* test both urandom and getrandom implementations
  • Loading branch information
whyoleg committed Mar 13, 2023
1 parent f11f58c commit 3e6e004
Show file tree
Hide file tree
Showing 8 changed files with 123 additions and 20 deletions.
8 changes: 8 additions & 0 deletions cryptography-random/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,12 @@ description = "cryptography-kotlin random API"

kotlin {
allTargets()

linuxX64 {
val main by compilations.getting {
val random by cinterops.creating {
defFile("linux.def")
}
}
}
}
2 changes: 2 additions & 0 deletions cryptography-random/linux.def
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
package = dev.whyoleg.cryptography.random.internal.cinterop
headers = linux/random.h
Original file line number Diff line number Diff line change
@@ -1,22 +1,3 @@
package dev.whyoleg.cryptography.random

import kotlinx.cinterop.*
import platform.posix.*

internal actual fun defaultCryptographyRandom(): CryptographyRandom = URandomCryptographyRandom

private object URandomCryptographyRandom : PlatformRandom() {
override fun fillBytes(array: ByteArray) {
val file = checkNotNull(fopen("/dev/urandom", "rb")) { "Failed to open /dev/urandom" }
val result = array.usePinned { pinned ->
fread(
__ptr = pinned.addressOf(0),
__size = 1.convert(),
__n = pinned.get().size.convert(),
__stream = file
)
}
fclose(file)
if (result <= 0UL) error("Failed to read from /dev/urandom")
}
}
internal actual fun defaultCryptographyRandom(): CryptographyRandom = createGetRandom() ?: createURandom()
28 changes: 28 additions & 0 deletions cryptography-random/main/sources/linux/GetRandom.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package dev.whyoleg.cryptography.random

import dev.whyoleg.cryptography.random.internal.cinterop.*
import kotlinx.cinterop.*
import platform.linux.*
import platform.posix.*

internal fun createGetRandom(): CryptographyRandom? = if (getRandomAvailable()) GetRandom else null

private object GetRandom : LinuxRandom() {
override fun fillBytes(pointer: CPointer<ByteVar>, size: Int): Int = getrandom(pointer, size.convert(), 0.convert())
}

private fun getRandomAvailable(): Boolean {
val stubArray = ByteArray(1)
val stubSize = stubArray.size
stubArray.usePinned {
if (getrandom(it.addressOf(0), stubSize.convert(), GRND_NONBLOCK.convert()) >= 0) return true
}

return when (errno) {
ENOSYS, EPERM -> false
else -> true
}
}

private fun getrandom(out: CPointer<ByteVar>?, outSize: size_t, flags: UInt): Int =
syscall(SYS_getrandom.convert(), out, outSize, flags).convert()
18 changes: 18 additions & 0 deletions cryptography-random/main/sources/linux/LinuxRandom.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package dev.whyoleg.cryptography.random

import kotlinx.cinterop.*

internal abstract class LinuxRandom : PlatformRandom() {
protected abstract fun fillBytes(pointer: CPointer<ByteVar>, size: Int): Int
final override fun fillBytes(array: ByteArray) {
val size = array.size
array.usePinned {
var filled = 0
while (filled < size) {
val chunkSize = fillBytes(it.addressOf(filled), size - filled)
if (chunkSize < 0) errnoCheck()
filled += chunkSize
}
}
}
}
48 changes: 48 additions & 0 deletions cryptography-random/main/sources/linux/URandom.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package dev.whyoleg.cryptography.random

import kotlinx.cinterop.*
import platform.posix.*

internal fun createURandom(): CryptographyRandom {
awaitURandomReady()
return URandom
}

private object URandom : LinuxRandom() {
override fun fillBytes(pointer: CPointer<ByteVar>, size: Int): Int = read(FD.value, pointer, size.convert()).convert()
}

@ThreadLocal
private object FD {
val value = open("/dev/urandom")
}

private fun awaitURandomReady() {
val randomFd = open("/dev/random")
try {
memScoped {
val pollFd = alloc<pollfd> {
fd = randomFd
events = POLLIN.convert()
revents = 0
}

while (true) {
if (poll(pollFd.ptr, 1, -1) >= 0) break

when (errno) {
EINTR, EAGAIN -> continue
else -> errnoCheck()
}
}
}
} finally {
close(randomFd)
}
}

private fun open(path: String): Int {
val fd = open(path, O_RDONLY, null)
if (fd <= 0) errnoCheck()
return fd
}
13 changes: 13 additions & 0 deletions cryptography-random/main/sources/linux/errno.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package dev.whyoleg.cryptography.random

import platform.posix.*

internal fun errnoCheck(): Nothing {
val message = when (val value = errno) {
EFAULT -> "The address referred to by buf is outside the accessible address space."
EINTR -> "The call was interrupted by a signal handler; see the description of how interrupted read(2) calls on 'slow' devices are handled with and without the SA_RESTART flag in the signal(7) man page."
EINVAL -> "An invalid flag was specified in flags."
else -> "POSIX error: $value"
}
error(message)
}
5 changes: 5 additions & 0 deletions cryptography-random/test/sources/linux/LinuxRandomTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package dev.whyoleg.cryptography.random

class URandomTest : CryptographyRandomTest(createURandom())

class GetRandomTest : CryptographyRandomTest(createGetRandom()!!)

0 comments on commit 3e6e004

Please sign in to comment.