Skip to content

Commit

Permalink
fix ios memcpy
Browse files Browse the repository at this point in the history
  • Loading branch information
JesusMcCloud committed Jan 26, 2025
1 parent 96bc668 commit 9217fcb
Show file tree
Hide file tree
Showing 9 changed files with 77 additions and 106 deletions.
8 changes: 3 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -231,7 +231,7 @@ println("Is it trustworthy? $isValid")
We currently support ChaCha20-Poly1503, AES-CBC, AES-GCM, and a very flexible flavour of AES-CBC-HMAC.
This is supported across all _Supreme_ targets and works as follows:
```kotlin
val payload = "More matter, with less art!".encodeToByteArray()
val payload = "More matter, with less art!".encodeToByteArray()

//define algorithm parameters
val algorithm = SymmetricEncryptionAlgorithm.AES_192.CBC.HMAC.SHA_512
Expand All @@ -241,7 +241,7 @@ val algorithm = SymmetricEncryptionAlgorithm.AES_192.CBC.HMAC.SHA_512
}

//any size is fine, really. omitting the override generates a mac key of the same size as the encryption key
val key = algorithm.randomKey(dedicatedMacKey = secureRandom.nextBytesOf(32))
val key = algorithm.randomKey(32.bit)
val aad = Clock.System.now().toString().encodeToByteArray()

val sealedBox = key.encrypt(
Expand Down Expand Up @@ -272,9 +272,7 @@ val reconstructed = algorithm.sealedBox(
authenticatedData = sealedBox.authenticatedData
)

val manuallyRecovered = reconstructed.decrypt(
key,
).getOrThrow(/*handle error*/)
val manuallyRecovered = reconstructed.decrypt(key).getOrThrow(/*handle error*/)

manuallyRecovered shouldBe payload //great success!

Expand Down
10 changes: 4 additions & 6 deletions docs/docs/supreme.md
Original file line number Diff line number Diff line change
Expand Up @@ -357,11 +357,11 @@ To minimise the potential for error, everything (algorithms, keys, sealed boxes)
Hence, a sealed box containing an authenticated ciphertext will only ever accept a symmetric key that is usable for AEAD.
Additional runtime checks ensure that mo mixups can happen.

Signum also support custom HMAC-based authenticated encryption, letting you freely define which data gets fed into the MAC.
Signum also supports custom HMAC-based authenticated encryption, letting you freely define which data gets fed into the MAC.
You also have free rein over the MAC key:

```kotlin
val payload = "More matter, with less art!".encodeToByteArray()
val payload = "More matter, with less art!".encodeToByteArray()

//define algorithm parameters
val algorithm = SymmetricEncryptionAlgorithm.AES_192.CBC.HMAC.SHA_512
Expand All @@ -371,7 +371,7 @@ val algorithm = SymmetricEncryptionAlgorithm.AES_192.CBC.HMAC.SHA_512
}

//any size is fine, really. omitting the override generates a mac key of the same size as the encryption key
val key = algorithm.randomKey(dedicatedMacKey = secureRandom.nextBytesOf(32))
val key = algorithm.randomKey(32.bit)
val aad = Clock.System.now().toString().encodeToByteArray()

val sealedBox = key.encrypt(
Expand Down Expand Up @@ -402,9 +402,7 @@ val reconstructed = algorithm.sealedBox(
authenticatedData = sealedBox.authenticatedData
)

val manuallyRecovered = reconstructed.decrypt(
key,
).getOrThrow(/*handle error*/)
val manuallyRecovered = reconstructed.decrypt(key).getOrThrow(/*handle error*/)

manuallyRecovered shouldBe payload //great success!

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ data class JsonWebKey(
* The "alg" (algorithm) parameter identifies the algorithm intended for
* use with the key. The values used should either be registered in the
* IANA "JSON Web Signature and Encryption Algorithms" registry
* established by [JWA] or be a value that contains a Collision-
* Resistant Name. The "alg" value is a case-sensitive ASCII string.
* established by [JWA] or be a value that contains a collision-resistant Name.
* The "alg" value is a case-sensitive ASCII string.
* Use of this member is OPTIONAL.
*/
@SerialName("alg")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,16 @@
package at.asitplus.signum.internals

import kotlinx.cinterop.*
import platform.CoreFoundation.CFDictionaryCreateMutable
import platform.CoreFoundation.CFDictionaryGetValue
import platform.CoreFoundation.CFDictionaryRef
import platform.CoreFoundation.CFDictionarySetValue
import platform.CoreFoundation.CFErrorRefVar
import platform.CoreFoundation.CFMutableDictionaryRef
import platform.CoreFoundation.CFTypeRef
import platform.CoreFoundation.kCFBooleanFalse
import platform.CoreFoundation.kCFBooleanTrue
import platform.CoreFoundation.kCFTypeDictionaryKeyCallBacks
import platform.CoreFoundation.kCFTypeDictionaryValueCallBacks
import platform.Foundation.CFBridgingRelease
import platform.Foundation.CFBridgingRetain
import platform.Foundation.NSData
import platform.Foundation.NSError
import platform.Foundation.create
import platform.CoreFoundation.*
import platform.Foundation.*
import platform.posix.memcpy

fun NSData.toByteArray(): ByteArray = ByteArray(length.toInt()).apply {
usePinned {
memcpy(it.addressOf(0), bytes, length)
}
if(length>Int.MAX_VALUE.toULong()) throw IndexOutOfBoundsException("length is too large")
if (length > 0uL)
usePinned {
memcpy(it.addressOf(0), bytes, length)
}
}

@OptIn(BetaInteropApi::class)
Expand All @@ -33,14 +21,14 @@ fun ByteArray.toNSData(): NSData = memScoped {
}

fun NSError.toNiceString(): String {
val sb = StringBuilder("[${if(domain != null) "$domain error, " else ""}code $code] $localizedDescription\n")
val sb = StringBuilder("[${if (domain != null) "$domain error, " else ""}code $code] $localizedDescription\n")
localizedFailureReason?.let { sb.append("Because: $it") }
localizedRecoverySuggestion?.let { sb.append("Try: $it") }
localizedRecoveryOptions?.let { sb.append("Try also:\n - ${it.joinToString("\n - ")}\n") }
return sb.toString()
}

class CoreFoundationException(val nsError: NSError): Throwable(nsError.toNiceString())
class CoreFoundationException(val nsError: NSError) : Throwable(nsError.toNiceString())
class corecall private constructor(val error: CPointer<CFErrorRefVar>) {
/** Helper for calling Core Foundation functions, and bridging exceptions across.
*
Expand All @@ -53,7 +41,7 @@ class corecall private constructor(val error: CPointer<CFErrorRefVar>) {
*/
companion object {
@OptIn(BetaInteropApi::class)
operator fun <T> invoke(call: corecall.()->T?) : T {
operator fun <T> invoke(call: corecall.() -> T?): T {
memScoped {
val errorH = alloc<CFErrorRefVar>()
val result = corecall(errorH.ptr).call()
Expand All @@ -62,13 +50,15 @@ class corecall private constructor(val error: CPointer<CFErrorRefVar>) {
(result != null) && (error == null) -> return result
(result == null) && (error != null) ->
throw CoreFoundationException(error.takeFromCF<NSError>())

else -> throw IllegalStateException("Invalid state returned by Core Foundation call")
}
}
}
}
}
class SwiftException(message: String): Throwable(message)

class SwiftException(message: String) : Throwable(message)
class swiftcall private constructor(val error: CPointer<ObjCObjectVar<NSError?>>) {
/** Helper for calling swift-objc-mapped functions, and bridging exceptions across.
*
Expand All @@ -81,7 +71,7 @@ class swiftcall private constructor(val error: CPointer<ObjCObjectVar<NSError?>>
*/
companion object {
@OptIn(BetaInteropApi::class)
operator fun <T> invoke(call: swiftcall.()->T?): T {
operator fun <T> invoke(call: swiftcall.() -> T?): T {
memScoped {
val errorH = alloc<ObjCObjectVar<NSError?>>()
val result = swiftcall(errorH.ptr).call()
Expand All @@ -97,34 +87,44 @@ class swiftcall private constructor(val error: CPointer<ObjCObjectVar<NSError?>>
}


inline fun <reified T: CFTypeRef?> Any?.giveToCF() = when(this) {
inline fun <reified T : CFTypeRef?> Any?.giveToCF() = when (this) {
null -> this
is Boolean -> if (this) kCFBooleanTrue else kCFBooleanFalse
is CValuesRef<*> -> this
else -> CFBridgingRetain(this)
} as T

inline fun <reified T> CFTypeRef?.takeFromCF() = CFBridgingRelease(this) as T
fun MemScope.cfDictionaryOf(vararg pairs: Pair<*,*>): CFDictionaryRef {
val dict = CFDictionaryCreateMutable(null, pairs.size.toLong(),
kCFTypeDictionaryKeyCallBacks.ptr, kCFTypeDictionaryValueCallBacks.ptr)!!
fun MemScope.cfDictionaryOf(vararg pairs: Pair<*, *>): CFDictionaryRef {
val dict = CFDictionaryCreateMutable(
null, pairs.size.toLong(),
kCFTypeDictionaryKeyCallBacks.ptr, kCFTypeDictionaryValueCallBacks.ptr
)!!
defer { CFBridgingRelease(dict) } // free it after the memscope finishes
pairs.forEach { (k,v) -> dict[k] = v }
pairs.forEach { (k, v) -> dict[k] = v }
return dict
}

class CFDictionaryInitScope private constructor() {
private val pairs = mutableListOf<Pair<*,*>>()
private val pairs = mutableListOf<Pair<*, *>>()

fun map(pair: Pair<*,*>) { pairs.add(pair) }
infix fun Any?.mapsTo(other: Any?) { map(this to other) }
fun map(pair: Pair<*, *>) {
pairs.add(pair)
}

infix fun Any?.mapsTo(other: Any?) {
map(this to other)
}

companion object {
fun resolve(scope: MemScope, fn: CFDictionaryInitScope.()->Unit) =
fun resolve(scope: MemScope, fn: CFDictionaryInitScope.() -> Unit) =
scope.cfDictionaryOf(*CFDictionaryInitScope().apply(fn).pairs.toTypedArray())
}
}
fun MemScope.createCFDictionary(pairs: CFDictionaryInitScope.()->Unit) =

fun MemScope.createCFDictionary(pairs: CFDictionaryInitScope.() -> Unit) =
CFDictionaryInitScope.resolve(this, pairs)

inline operator fun <reified T> CFDictionaryRef.get(key: Any?): T =
CFDictionaryGetValue(this, key.giveToCF()).takeFromCF<T>()

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ private fun SymmetricEncryptionAlgorithm<*, *, *>.keyFromInternal(
dedicatedMacKey: ByteArray?
): SymmetricKey<*, *, *> {
require(bytes.size == this.keySize.bytes.toInt()) { "Invalid key size: ${bytes.size * 8}. Required: keySize=${bytes.size.bitLength}" }
dedicatedMacKey?.let {
require(it.isNotEmpty()) { "Dedicated MAC key is empty!" }
}
@OptIn(HazardousMaterials::class)
return when (this.requiresNonce()) {
true -> when (isAuthenticated()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,10 @@ fun <K : KeyType, A : AuthType<out K>> KeyWithNonce<A, out K>.encrypt(
*/
@HazardousMaterials("Nonce/IV re-use can have catastrophic consequences!")
fun <K : KeyType, A : AuthType<out K>> SymmetricKey<A, Nonce.Required, out K>.andPredefinedNonce(nonce: ByteArray) =
KeyWithNonce(this, nonce)
catching {
require(nonce.size == algorithm.nonce.length.bytes.toInt()) { "Nonce is empty!" }
KeyWithNonce(this, nonce)
}

/**
* This function can be used to feed a pre-set nonce into encryption functions.
Expand All @@ -64,8 +67,11 @@ fun <K : KeyType, A : AuthType<out K>> SymmetricKey<A, Nonce.Required, out K>.an
*/
@HazardousMaterials("Nonce/IV re-use can have catastrophic consequences!")
@JvmName("authedKeyWithNonce")
fun <K: KeyType, A : AuthType.Authenticated< out K>> SymmetricKey<out A, Nonce.Required,out K>.andPredefinedNonce(nonce: ByteArray) =
KeyWithNonceAuthenticating(nonce, this)
fun <K : KeyType, A : AuthType.Authenticated<out K>> SymmetricKey<out A, Nonce.Required, out K>.andPredefinedNonce(nonce: ByteArray) =
catching {
require(nonce.size == algorithm.nonce.length.bytes.toInt()) { "Invalid nonce size!" }
KeyWithNonceAuthenticating(nonce, this)
}

private typealias KeyWithNonce<A, K> = Pair<SymmetricKey<out A, Nonce.Required, out K>, ByteArray>
//types first and second are deliberately swapped
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,15 +61,15 @@ class `0SymmetricTest` : FreeSpec({
Random.nextBytes(1),
Random.nextBytes(17),
Random.nextBytes(18),
Random.nextBytes(32),
Random.nextBytes(33),
Random.nextBytes(256),
null
) { iv ->

val key = alg.randomKey()
if (iv != null) key.andPredefinedNonce(iv).encrypt(Random.nextBytes(32)) shouldNot succeed
if (iv != null) key.andPredefinedNonce(iv) shouldNot succeed
else key.encrypt(Random.nextBytes(32)) should succeed
key.andPredefinedNonce(alg.randomNonce()).encrypt(Random.nextBytes(32)) should succeed
key.andPredefinedNonce(alg.randomNonce()).getOrThrow().encrypt(Random.nextBytes(32)) should succeed
key.encrypt(Random.nextBytes(32)) should succeed

if (alg.authCapability is AuthType.Authenticated)
Expand Down Expand Up @@ -136,7 +136,7 @@ class `0SymmetricTest` : FreeSpec({


key.encrypt(Random.nextBytes(32)) should succeed
key.andPredefinedNonce(alg.randomNonce()).encrypt(data = Random.nextBytes(32)) should succeed
key.andPredefinedNonce(alg.randomNonce()).getOrThrow().encrypt(data = Random.nextBytes(32)) should succeed

if (alg.authCapability is AuthType.Authenticated)
alg.randomKey().encrypt(
Expand Down Expand Up @@ -188,7 +188,7 @@ class `0SymmetricTest` : FreeSpec({
) { iv ->

val ciphertext =
if (iv != null) key.andPredefinedNonce(iv).encrypt(plaintext).getOrThrow()
if (iv != null) key.andPredefinedNonce(iv).getOrThrow().encrypt(plaintext).getOrThrow()
else key.encrypt(plaintext).getOrThrow()

ciphertext.nonce.shouldNotBeNull()
Expand Down Expand Up @@ -282,7 +282,7 @@ class `0SymmetricTest` : FreeSpec({
) { aad ->
key.encrypt(plaintext, aad)
val ciphertext =
if (iv != null) key.andPredefinedNonce(iv).encrypt(plaintext, aad).getOrThrow()
if (iv != null) key.andPredefinedNonce(iv).getOrThrow().encrypt(plaintext, aad).getOrThrow()
else key.encrypt(plaintext, aad).getOrThrow()

ciphertext.nonce.shouldNotBeNull()
Expand Down Expand Up @@ -410,23 +410,23 @@ class `0SymmetricTest` : FreeSpec({
null
) { aad ->
val ciphertext =
if (iv != null) key.andPredefinedNonce(iv).encrypt(plaintext, aad).getOrThrow()
if (iv != null) key.andPredefinedNonce(iv).getOrThrow().encrypt(plaintext, aad).getOrThrow()
else key.encrypt(plaintext, aad).getOrThrow()
val manilaAlg = it.Custom { _, _, _ -> "Manila".encodeToByteArray() }
val manilaKey = SymmetricKey.WithDedicatedMac.RequiringNonce(
manilaAlg,
key.secretKey,
key.dedicatedMacKey
)
if (iv != null) manilaKey.andPredefinedNonce(iv).encrypt(plaintext, aad)
if (iv != null) manilaKey.andPredefinedNonce(iv).getOrThrow().encrypt(plaintext, aad)
.getOrThrow() shouldNotBe ciphertext
manilaKey.encrypt(plaintext, aad).getOrThrow() shouldNotBe ciphertext

//no randomness. must be equal
val randomIV = it.randomNonce()
manilaKey.andPredefinedNonce(randomIV).encrypt(plaintext, aad)
manilaKey.andPredefinedNonce(randomIV).getOrThrow().encrypt(plaintext, aad)
.getOrThrow() shouldBe
manilaKey.andPredefinedNonce(randomIV).encrypt(plaintext, aad)
manilaKey.andPredefinedNonce(randomIV).getOrThrow().encrypt(plaintext, aad)
.getOrThrow()

if (iv != null) ciphertext.nonce shouldBe iv
Expand Down
Loading

0 comments on commit 9217fcb

Please sign in to comment.