Kotlin Platform Guide

This guide covers the installation, setup, and usage of MetaMUI Crypto Primitives in Kotlin projects for JVM and Android.

Installation

Build from source:

cd metamui-crypto-kotlin
./gradlew build

Running Tests

cd metamui-crypto-kotlin
./gradlew test --no-daemon

Using in Your Project

To use as a local dependency, add a project reference or include the built JAR. For a composite Gradle build, add to your settings.gradle.kts:

includeBuild("../metamui-crypto-kotlin")

Then in your build.gradle.kts:

dependencies {
    implementation("id.metamui:crypto-kotlin")
}

Quick Start

import id.metamui.crypto.*

fun main() {
    // Generate Ed25519 keypair
    val keypair = Ed25519.generateKeypair()

    // Sign a message
    val message = "Hello, MetaMUI!".toByteArray()
    val signature = Ed25519.sign(message, keypair.privateKey)

    // Verify signature
    val isValid = Ed25519.verify(signature, message, keypair.publicKey)
    println("Signature valid: $isValid")

    // Encrypt with ChaCha20-Poly1305
    val key = ChaCha20Poly1305.generateKey()
    val cipher = ChaCha20Poly1305(key)

    val plaintext = "Secret message".toByteArray()
    val nonce = ChaCha20Poly1305.generateNonce()

    val (ciphertext, tag) = cipher.encrypt(plaintext, nonce)

    // Decrypt
    val decrypted = cipher.decrypt(ciphertext, tag, nonce)
    println("Decrypted: ${String(decrypted)}")
}

Android Integration

Android Keystore

import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import id.metamui.crypto.*
import java.security.KeyStore
import javax.crypto.SecretKey

class AndroidCryptoManager(private val context: Context) {
    companion object {
        private const val KEYSTORE_ALIAS = "MetaMUICrypto"
        private const val ANDROID_KEYSTORE = "AndroidKeyStore"
    }

    private val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply {
        load(null)
    }

    fun generateAndStoreKey() {
        val keyGenerator = javax.crypto.KeyGenerator.getInstance(
            KeyProperties.KEY_ALGORITHM_AES,
            ANDROID_KEYSTORE
        )

        val spec = KeyGenParameterSpec.Builder(
            KEYSTORE_ALIAS,
            KeyProperties.PURPOSE_ENCRYPT or KeyProperties.PURPOSE_DECRYPT
        )
            .setBlockModes(KeyProperties.BLOCK_MODE_GCM)
            .setEncryptionPaddings(KeyProperties.ENCRYPTION_PADDING_NONE)
            .setUserAuthenticationRequired(true)
            .setUserAuthenticationValidityDurationSeconds(30)
            .build()

        keyGenerator.init(spec)
        keyGenerator.generateKey()
    }

    fun encryptWithBiometrics(data: ByteArray): EncryptedData {
        val secretKey = keyStore.getKey(KEYSTORE_ALIAS, null) as SecretKey

        // Use MetaMUI for actual encryption with derived key
        val derivedKey = deriveKeyFromAndroidKey(secretKey)
        val cipher = ChaCha20Poly1305(derivedKey)
        val nonce = ChaCha20Poly1305.generateNonce()

        val (ciphertext, tag) = cipher.encrypt(data, nonce)

        return EncryptedData(ciphertext, tag, nonce)
    }
}

Jetpack Compose Integration

import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import id.metamui.crypto.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class CryptoViewModel : ViewModel() {
    private var _publicKey by mutableStateOf("")
    val publicKey: String get() = _publicKey

    private var _isLoading by mutableStateOf(false)
    val isLoading: Boolean get() = _isLoading

    private var keypair: Ed25519.Keypair? = null

    fun generateKeypair() {
        viewModelScope.launch {
            _isLoading = true
            try {
                keypair = withContext(Dispatchers.Default) {
                    Ed25519.generateKeypair()
                }
                _publicKey = keypair?.publicKey?.toHex() ?: ""
            } finally {
                _isLoading = false
            }
        }
    }

    suspend fun signMessage(message: String): String? {
        return withContext(Dispatchers.Default) {
            keypair?.let { kp ->
                val signature = Ed25519.sign(message.toByteArray(), kp.privateKey)
                signature.toHex()
            }
        }
    }
}

@Composable
fun CryptoScreen(viewModel: CryptoViewModel = viewModel()) {
    var message by remember { mutableStateOf("") }
    var signature by remember { mutableStateOf("") }

    Column(
        modifier = Modifier
            .fillMaxSize()
            .padding(16.dp),
        verticalArrangement = Arrangement.spacedBy(16.dp)
    ) {
        Button(
            onClick = { viewModel.generateKeypair() },
            enabled = !viewModel.isLoading
        ) {
            Text("Generate Keypair")
        }

        if (viewModel.isLoading) {
            CircularProgressIndicator()
        }

        if (viewModel.publicKey.isNotEmpty()) {
            Card {
                Column(modifier = Modifier.padding(16.dp)) {
                    Text("Public Key:", style = MaterialTheme.typography.h6)
                    Text(
                        viewModel.publicKey,
                        style = MaterialTheme.typography.body2,
                        fontFamily = FontFamily.Monospace
                    )
                }
            }

            OutlinedTextField(
                value = message,
                onValueChange = { message = it },
                label = { Text("Message to sign") },
                modifier = Modifier.fillMaxWidth()
            )

            Button(
                onClick = {
                    viewModel.viewModelScope.launch {
                        viewModel.signMessage(message)?.let {
                            signature = it
                        }
                    }
                },
                enabled = message.isNotEmpty()
            ) {
                Text("Sign Message")
            }

            if (signature.isNotEmpty()) {
                Card(
                    modifier = Modifier.fillMaxWidth(),
                    colors = CardDefaults.cardColors(
                        containerColor = MaterialTheme.colorScheme.secondaryContainer
                    )
                ) {
                    Column(modifier = Modifier.padding(16.dp)) {
                        Text("Signature:", style = MaterialTheme.typography.h6)
                        Text(
                            signature,
                            style = MaterialTheme.typography.body2,
                            fontFamily = FontFamily.Monospace
                        )
                    }
                }
            }
        }
    }
}

Coroutines Support

import id.metamui.crypto.*
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.*

class CryptoService {
    fun generateKeypairFlow(): Flow<Ed25519.Keypair> = flow {
        emit(Ed25519.generateKeypair())
    }.flowOn(Dispatchers.Default)

    suspend fun encryptAsync(
        data: ByteArray,
        key: ByteArray
    ): EncryptedData = withContext(Dispatchers.Default) {
        val cipher = ChaCha20Poly1305(key)
        val nonce = ChaCha20Poly1305.generateNonce()
        val (ciphertext, tag) = cipher.encrypt(data, nonce)
        EncryptedData(ciphertext, tag, nonce)
    }

    fun encryptStream(dataFlow: Flow<ByteArray>, key: ByteArray): Flow<EncryptedData> {
        val cipher = ChaCha20Poly1305(key)

        return dataFlow
            .map { chunk ->
                val nonce = ChaCha20Poly1305.generateNonce()
                val (ciphertext, tag) = cipher.encrypt(chunk, nonce)
                EncryptedData(ciphertext, tag, nonce)
            }
            .flowOn(Dispatchers.Default)
    }
}

// Usage
fun main() = runBlocking {
    val cryptoService = CryptoService()

    // Generate keypair with Flow
    cryptoService.generateKeypairFlow()
        .collect { keypair ->
            println("Generated keypair: ${keypair.publicKey.toHex()}")
        }

    // Encrypt data asynchronously
    val key = ChaCha20Poly1305.generateKey()
    val encrypted = cryptoService.encryptAsync("Secret data".toByteArray(), key)

    // Process stream of data
    val dataStream = (1..10).asFlow().map { "Chunk $it".toByteArray() }

    cryptoService.encryptStream(dataStream, key)
        .collect { encryptedChunk ->
            println("Encrypted chunk: ${encryptedChunk.ciphertext.size} bytes")
        }
}

Ktor Integration

import io.ktor.server.application.*
import io.ktor.server.request.*
import io.ktor.server.response.*
import io.ktor.server.routing.*
import io.ktor.http.*
import id.metamui.crypto.*
import kotlinx.serialization.Serializable

@Serializable
data class EncryptRequest(val data: String)

@Serializable
data class EncryptResponse(
    val ciphertext: String,
    val tag: String,
    val nonce: String
)

fun Application.cryptoModule() {
    val encryptionKey = environment.config.property("crypto.key").getString().fromHex()
    val cipher = ChaCha20Poly1305(encryptionKey)

    routing {
        route("/api/crypto") {
            post("/encrypt") {
                val request = call.receive<EncryptRequest>()

                val plaintext = request.data.toByteArray()
                val nonce = ChaCha20Poly1305.generateNonce()

                val (ciphertext, tag) = cipher.encrypt(plaintext, nonce)

                call.respond(EncryptResponse(
                    ciphertext = ciphertext.toHex(),
                    tag = tag.toHex(),
                    nonce = nonce.toHex()
                ))
            }

            post("/sign") {
                val privateKey = environment.config.property("crypto.signing.key").getString().fromHex()
                val message = call.receiveText().toByteArray()

                val signature = Ed25519.sign(message, privateKey)

                call.respond(mapOf(
                    "signature" to signature.toHex()
                ))
            }
        }
    }
}

Testing

JUnit 5 Tests

import org.junit.jupiter.api.Test
import org.junit.jupiter.api.Assertions.*
import org.junit.jupiter.api.assertThrows
import id.metamui.crypto.*

class CryptoTest {
    @Test
    fun `Ed25519 signature roundtrip`() {
        val keypair = Ed25519.generateKeypair()
        val message = "Test message".toByteArray()

        val signature = Ed25519.sign(message, keypair.privateKey)

        assertTrue(Ed25519.verify(signature, message, keypair.publicKey))
    }

    @Test
    fun `ChaCha20Poly1305 encryption with AAD`() {
        val key = ChaCha20Poly1305.generateKey()
        val cipher = ChaCha20Poly1305(key)
        val plaintext = "Secret data".toByteArray()
        val nonce = ChaCha20Poly1305.generateNonce()
        val aad = "metadata".toByteArray()

        val (ciphertext, tag) = cipher.encrypt(plaintext, nonce, aad)
        val decrypted = cipher.decrypt(ciphertext, tag, nonce, aad)

        assertArrayEquals(plaintext, decrypted)

        // Wrong AAD should fail
        assertThrows<CryptoException> {
            cipher.decrypt(ciphertext, tag, nonce, "wrong".toByteArray())
        }
    }
}

Extension Functions

// Useful extensions
fun ByteArray.toHex(): String = joinToString("") { "%02x".format(it) }

fun String.fromHex(): ByteArray {
    require(length % 2 == 0) { "Hex string must have even length" }
    return chunked(2).map { it.toInt(16).toByte() }.toByteArray()
}

fun ByteArray.secureEquals(other: ByteArray): Boolean {
    if (size != other.size) return false
    var result = 0
    for (i in indices) {
        result = result or (this[i].toInt() xor other[i].toInt())
    }
    return result == 0
}

inline fun <T> ByteArray.use(block: (ByteArray) -> T): T {
    return try {
        block(this)
    } finally {
        fill(0)
    }
}

// Usage
val secretKey = ChaCha20Poly1305.generateKey()
secretKey.use { key ->
    val cipher = ChaCha20Poly1305(key)
    // Use cipher...
} // Key is cleared after use

Best Practices

  1. Use ByteArray.fill(0) to clear sensitive data
  2. Prefer SecureRandom over Random for crypto
  3. Use @OptIn(ExperimentalUnsignedTypes::class) for unsigned operations
  4. Enable R8/ProGuard optimization for release builds
  5. Use inline functions for performance-critical code
  6. Implement proper error handling with sealed classes
  7. Use coroutines for async crypto operations

Troubleshooting

Common Issues

  1. NoSuchAlgorithmException
    // Ensure you're using MetaMUI implementations
    // Not javax.crypto or java.security
    import id.metamui.crypto.* // Correct
    // import javax.crypto.* // Wrong
    
  2. OutOfMemoryError with large files
    // Use streaming instead of loading entire file
    // Bad: file.readBytes()
    // Good: Use chunked processing
    
  3. Slow performance on Android
    // Move crypto operations off main thread
    lifecycleScope.launch(Dispatchers.Default) {
        // Crypto operations here
    }
    

Resources