Kotlin Platform Guide

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

Installation

Gradle (Kotlin DSL)

// build.gradle.kts
dependencies {
    implementation("id.metamui:crypto-kotlin:0.1.0")
    
    // Or specific algorithms
    implementation("id.metamui:crypto-kotlin-ed25519:0.1.0")
    implementation("id.metamui:crypto-kotlin-chacha20:0.1.0")
    implementation("id.metamui:crypto-kotlin-post-quantum:0.1.0")
}

Gradle (Groovy)

// build.gradle
dependencies {
    implementation 'id.metamui:crypto-kotlin:0.1.0'
    
    // Or specific algorithms
    implementation 'id.metamui:crypto-kotlin-ed25519:0.1.0'
    implementation 'id.metamui:crypto-kotlin-chacha20:0.1.0'
    implementation 'id.metamui:crypto-kotlin-post-quantum:0.1.0'
}

Maven

<dependency>
    <groupId>id.metamui</groupId>
    <artifactId>crypto-kotlin</artifactId>
    <version>0.1.0</version>
</dependency>

Kotlin Multiplatform

// build.gradle.kts
kotlin {
    sourceSets {
        val commonMain by getting {
            dependencies {
                implementation("id.metamui:crypto-kotlin-multiplatform:0.1.0")
            }
        }
        val androidMain by getting {
            dependencies {
                implementation("id.metamui:crypto-kotlin-android:0.1.0")
            }
        }
        val iosMain by getting {
            dependencies {
                implementation("id.metamui:crypto-kotlin-ios:0.1.0")
            }
        }
    }
}

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()
                ))
            }
        }
    }
}

// Authentication plugin
val CryptoAuth = createApplicationPlugin(name = "CryptoAuth") {
    val publicKey = environment.config.property("crypto.auth.publicKey").getString().fromHex()
    
    onCall { call ->
        val signature = call.request.header("X-Signature") ?: run {
            call.respond(HttpStatusCode.Unauthorized, "Missing signature")
            return@onCall
        }
        
        val timestamp = call.request.header("X-Timestamp") ?: run {
            call.respond(HttpStatusCode.Unauthorized, "Missing timestamp")
            return@onCall
        }
        
        // Verify timestamp is recent
        val now = System.currentTimeMillis()
        val requestTime = timestamp.toLongOrNull() ?: 0
        if (kotlin.math.abs(now - requestTime) > 60000) { // 1 minute
            call.respond(HttpStatusCode.Unauthorized, "Request expired")
            return@onCall
        }
        
        // Construct message to verify
        val method = call.request.httpMethod.value
        val path = call.request.path()
        val body = call.receiveText()
        val message = "$method:$path:$timestamp:$body".toByteArray()
        
        val isValid = Ed25519.verify(signature.fromHex(), message, publicKey)
        if (!isValid) {
            call.respond(HttpStatusCode.Unauthorized, "Invalid signature")
            return@onCall
        }
    }
}

Multiplatform Support

// Common code
expect class SecureRandom() {
    fun nextBytes(size: Int): ByteArray
}

expect object Platform {
    val name: String
    fun currentTimeMillis(): Long
}

// Common crypto operations
class MultiplatformCrypto {
    private val random = SecureRandom()
    
    fun generateKey(): ByteArray {
        return random.nextBytes(32)
    }
    
    fun encrypt(data: ByteArray, key: ByteArray): EncryptedData {
        val cipher = ChaCha20Poly1305(key)
        val nonce = random.nextBytes(12)
        val (ciphertext, tag) = cipher.encrypt(data, nonce)
        
        return EncryptedData(
            ciphertext = ciphertext,
            tag = tag,
            nonce = nonce,
            timestamp = Platform.currentTimeMillis()
        )
    }
}

// JVM implementation
actual class SecureRandom {
    private val jvmRandom = java.security.SecureRandom()
    
    actual fun nextBytes(size: Int): ByteArray {
        return ByteArray(size).also { jvmRandom.nextBytes(it) }
    }
}

actual object Platform {
    actual val name: String = "JVM"
    actual fun currentTimeMillis(): Long = System.currentTimeMillis()
}

// Android implementation
actual class SecureRandom {
    actual fun nextBytes(size: Int): ByteArray {
        return ByteArray(size).also { 
            android.security.SecureRandom().nextBytes(it)
        }
    }
}

actual object Platform {
    actual val name: String = "Android"
    actual fun currentTimeMillis(): Long = System.currentTimeMillis()
}

// iOS implementation
actual class SecureRandom {
    actual fun nextBytes(size: Int): ByteArray {
        return ByteArray(size).also { bytes ->
            bytes.usePinned { pinned ->
                SecRandomCopyBytes(kSecRandomDefault, size, pinned.addressOf(0))
            }
        }
    }
}

actual object Platform {
    actual val name: String = "iOS"
    actual fun currentTimeMillis(): Long = NSDate().timeIntervalSince1970.toLong() * 1000
}

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())
        }
    }
    
    @Test
    fun `Performance benchmark`() {
        val keypair = Ed25519.generateKeypair()
        val message = ByteArray(1024) { it.toByte() }
        
        val startTime = System.nanoTime()
        repeat(1000) {
            Ed25519.sign(message, keypair.privateKey)
        }
        val duration = System.nanoTime() - startTime
        
        println("1000 signatures in ${duration / 1_000_000}ms")
        assertTrue(duration < 1_000_000_000) // Less than 1 second
    }
}

Parameterized Tests

import org.junit.jupiter.params.ParameterizedTest
import org.junit.jupiter.params.provider.MethodSource
import java.util.stream.Stream

class AlgorithmTest {
    @ParameterizedTest
    @MethodSource("algorithms")
    fun `all algorithms should handle empty input`(algorithm: Algorithm) {
        val emptyData = ByteArray(0)
        
        when (algorithm) {
            is HashAlgorithm -> {
                val hash = algorithm.hash(emptyData)
                assertEquals(algorithm.digestSize, hash.size)
            }
            is EncryptionAlgorithm -> {
                val key = algorithm.generateKey()
                val nonce = algorithm.generateNonce()
                val (ciphertext, tag) = algorithm.encrypt(emptyData, key, nonce)
                val decrypted = algorithm.decrypt(ciphertext, tag, key, nonce)
                assertArrayEquals(emptyData, decrypted)
            }
        }
    }
    
    companion object {
        @JvmStatic
        fun algorithms(): Stream<Algorithm> = Stream.of(
            SHA256,
            SHA512,
            Blake2b,
            Blake3,
            ChaCha20Poly1305,
            AES256GCM
        )
    }
}

Performance Optimization

Parallel Processing

import kotlinx.coroutines.*
import java.util.concurrent.Executors

class ParallelCrypto {
    private val cryptoDispatcher = Executors.newFixedThreadPool(
        Runtime.getRuntime().availableProcessors()
    ).asCoroutineDispatcher()
    
    suspend fun encryptFiles(files: List<File>, key: ByteArray): List<EncryptedFile> {
        return withContext(cryptoDispatcher) {
            files.map { file ->
                async {
                    encryptFile(file, key)
                }
            }.awaitAll()
        }
    }
    
    private suspend fun encryptFile(file: File, key: ByteArray): EncryptedFile {
        return withContext(Dispatchers.IO) {
            val cipher = ChaCha20Poly1305(key)
            val plaintext = file.readBytes()
            val nonce = ChaCha20Poly1305.generateNonce()
            
            val (ciphertext, tag) = cipher.encrypt(plaintext, nonce)
            
            val encryptedFile = File(file.absolutePath + ".enc")
            encryptedFile.writeBytes(nonce + tag + ciphertext)
            
            EncryptedFile(
                originalName = file.name,
                encryptedPath = encryptedFile.absolutePath,
                size = encryptedFile.length()
            )
        }
    }
    
    fun close() {
        cryptoDispatcher.close()
    }
}

Memory-Efficient Streaming

import java.io.*
import java.nio.ByteBuffer
import java.nio.channels.ReadableByteChannel
import java.nio.channels.WritableByteChannel

class StreamingCrypto {
    fun encryptStream(
        input: InputStream,
        output: OutputStream,
        key: ByteArray,
        chunkSize: Int = 64 * 1024 // 64KB chunks
    ) {
        val cipher = ChaCha20Poly1305(key)
        val buffer = ByteArray(chunkSize)
        
        input.use { inStream ->
            output.use { outStream ->
                var chunkIndex = 0
                
                while (true) {
                    val bytesRead = inStream.read(buffer)
                    if (bytesRead == -1) break
                    
                    val chunk = if (bytesRead < chunkSize) {
                        buffer.copyOf(bytesRead)
                    } else {
                        buffer
                    }
                    
                    // Derive per-chunk nonce
                    val nonce = deriveChunkNonce(chunkIndex++)
                    val (ciphertext, tag) = cipher.encrypt(chunk, nonce)
                    
                    // Write chunk format: [4 bytes length][12 bytes nonce][16 bytes tag][ciphertext]
                    outStream.write(ByteBuffer.allocate(4).putInt(ciphertext.size).array())
                    outStream.write(nonce)
                    outStream.write(tag)
                    outStream.write(ciphertext)
                }
            }
        }
    }
    
    private fun deriveChunkNonce(index: Int): ByteArray {
        val nonce = ByteArray(12)
        ByteBuffer.wrap(nonce).putInt(8, index)
        return nonce
    }
}

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
    }
    

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

Resources