Text Encryptor — Kotlin source
Encrypt or decrypt text with a password using AES-256-GCM. Share the ciphertext safely - only someone with the password can read it.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// Text Encryptor — password-based text encryption (PBKDF2 + AES-256-GCM).
// Language: Kotlin (JVM 17+), standard library + javax.crypto (JVM native crypto).
// Ported from src/lib/text-encryptor.ts — display source, part of CosmoDev's
// polyglot tool pages. Functionally equivalent to the TS reference: a Base64
// string encrypted by one side decrypts on the other.
//
// Format: PBKDF2 (SHA-256, 100k iterations, 16-byte random salt) derives an
// AES-256 key from the password; AES-GCM encrypts with a 12-byte random IV.
// The Base64 output packs salt + IV + ciphertext (+ GCM tag), so every
// encryption is unique and self-contained - decrypt needs only the string and
// the password.
import java.security.SecureRandom
import javax.crypto.Cipher
import javax.crypto.SecretKeyFactory
import javax.crypto.spec.GCMParameterSpec
import javax.crypto.spec.PBEKeySpec
import javax.crypto.spec.SecretKeySpec
private const val PBKDF2_ITERATIONS = 100_000
private const val SALT_BYTES = 16
private const val IV_BYTES = 12
/** Smallest valid packed payload: salt + IV + one AES-GCM block (tag). */
private const val MIN_BYTES = SALT_BYTES + IV_BYTES + 16
private const val ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"
private val random = SecureRandom()
/** ByteArray -> canonical Base64 (pure codec, mirroring the TS implementation). */
fun bytesToBase64(bytes: ByteArray): String {
val out = StringBuilder()
var i = 0
while (i < bytes.size) {
val b0 = bytes[i].toInt() and 0xff
val b1 = if (i + 1 < bytes.size) bytes[i + 1].toInt() and 0xff else null
val b2 = if (i + 2 < bytes.size) bytes[i + 2].toInt() and 0xff else null
out.append(ALPHABET[b0 shr 2])
out.append(ALPHABET[((b0 and 0x03) shl 4) or ((b1 ?: 0) shr 4)])
out.append(if (b1 == null) '=' else ALPHABET[((b1 and 0x0f) shl 2) or ((b2 ?: 0) shr 6)])
out.append(if (b2 == null) '=' else ALPHABET[b2 and 0x3f])
i += 3
}
return out.toString()
}
/**
* Base64 -> ByteArray. Accepts surrounding whitespace; throws on any other
* non-alphabet character, wrong length, or misplaced padding.
*/
fun base64ToBytes(b64: String): ByteArray {
val clean = b64.replace(Regex("\\s+"), "")
if (clean.isEmpty()) throw IllegalArgumentException("Invalid Base64: input is empty")
if (clean.length % 4 != 0) throw IllegalArgumentException("Invalid Base64: length must be a multiple of 4")
val bad = clean.firstOrNull { ALPHABET.indexOf(it) < 0 && it != '=' }
if (bad != null) throw IllegalArgumentException("Invalid Base64: unexpected character \"$bad\"")
var outLength = (clean.length / 4) * 3
if (clean.endsWith("==")) outLength -= 2 else if (clean.endsWith("=")) outLength -= 1
val bytes = ByteArray(outLength)
var p = 0
var i = 0
while (i < clean.length) {
val c0 = ALPHABET.indexOf(clean[i])
val c1 = ALPHABET.indexOf(clean[i + 1])
val c2 = if (clean[i + 2] == '=') -1 else ALPHABET.indexOf(clean[i + 2])
val c3 = if (clean[i + 3] == '=') -1 else ALPHABET.indexOf(clean[i + 3])
if (p < outLength) bytes[p++] = ((c0 shl 2) or (c1 shr 4)).toByte()
if (c2 != -1 && p < outLength) bytes[p++] = (((c1 and 0x0f) shl 4) or (c2 shr 2)).toByte()
if (c2 != -1 && c3 != -1 && p < outLength) bytes[p++] = (((c2 and 0x03) shl 6) or c3).toByte()
i += 4
}
return bytes
}
/** PBKDF2-SHA256 (100k iterations) -> AES-256-GCM key. */
private fun deriveKey(password: String, salt: ByteArray): SecretKeySpec {
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val spec = PBEKeySpec(password.toCharArray(), salt, PBKDF2_ITERATIONS, 256)
return SecretKeySpec(factory.generateSecret(spec).encoded, "AES")
}
/**
* Encrypt UTF-8 `plaintext` under `password` and return the Base64 string
* packing salt + IV + AES-256-GCM ciphertext. Throws if either input is empty.
*/
fun encryptText(plaintext: String, password: String): String {
if (plaintext.isEmpty()) throw IllegalArgumentException("Nothing to encrypt: input is empty")
if (password.isEmpty()) throw IllegalArgumentException("Password must not be empty")
val salt = ByteArray(SALT_BYTES).also(random::nextBytes)
val iv = ByteArray(IV_BYTES).also(random::nextBytes)
val key = deriveKey(password, salt)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(128, iv))
val ciphertext = cipher.doFinal(plaintext.toByteArray(Charsets.UTF_8))
val packed = ByteArray(SALT_BYTES + IV_BYTES + ciphertext.size)
salt.copyInto(packed, 0)
iv.copyInto(packed, SALT_BYTES)
ciphertext.copyInto(packed, SALT_BYTES + IV_BYTES)
return bytesToBase64(packed)
}
/**
* Decrypt a Base64 string produced by [encryptText] back to plaintext. Throws
* on empty input/password, invalid Base64, a payload too short to be one of
* ours, or a GCM authentication failure (wrong password / tampered data).
*/
fun decryptText(ciphertext: String, password: String): String {
if (ciphertext.trim().isEmpty()) throw IllegalArgumentException("Nothing to decrypt: input is empty")
if (password.isEmpty()) throw IllegalArgumentException("Password must not be empty")
val packed = base64ToBytes(ciphertext.trim())
if (packed.size < MIN_BYTES) {
throw IllegalArgumentException("Ciphertext too short - not a valid salt + IV + AES-GCM payload")
}
val salt = packed.copyOfRange(0, SALT_BYTES)
val iv = packed.copyOfRange(SALT_BYTES, SALT_BYTES + IV_BYTES)
val data = packed.copyOfRange(SALT_BYTES + IV_BYTES, packed.size)
val key = deriveKey(password, salt)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(128, iv))
return try {
cipher.doFinal(data).toString(Charsets.UTF_8)
} catch (e: Exception) {
throw IllegalArgumentException("Decryption failed - wrong password or corrupted ciphertext", e)
}
}
Also available in 8 other languages
Every CosmoDev tool ships its pure logic in TypeScript (web) and Go (CLI), with authored implementations in a dozen-plus languages — the same contract, ported. Compare all languages side by side →