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