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
- Use
ByteArray.fill(0)to clear sensitive data - Prefer
SecureRandomoverRandomfor crypto - Use
@OptIn(ExperimentalUnsignedTypes::class)for unsigned operations - Enable R8/ProGuard optimization for release builds
- Use
inlinefunctions for performance-critical code - Implement proper error handling with sealed classes
- Use coroutines for async crypto operations
Troubleshooting
Common Issues
- NoSuchAlgorithmException
// Ensure you're using MetaMUI implementations // Not javax.crypto or java.security import id.metamui.crypto.* // Correct // import javax.crypto.* // Wrong - OutOfMemoryError with large files
// Use streaming instead of loading entire file // Bad: file.readBytes() // Good: Use chunked processing - 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