File Encryptor — Kotlin source
Encrypt or decrypt any file with AES-256-GCM in your browser. Password-based, zero server contact. Drag, drop, done.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// File Encryptor — password-based AES-256-GCM file encryption.
// Language: Kotlin (JVM 17+), standard library + javax.crypto (JVM native crypto).
// Ported from src/lib/file-encryptor.ts — display source, part of CosmoDev's
// polyglot tool pages. Functionally equivalent to the TS reference: the same
// password and input produce interoperable salt || IV || ciphertext+tag bytes.
//
// Wire format: salt (16 B) || IV (12 B) || AES-256-GCM ciphertext + tag.
// The 256-bit key is derived from the password with PBKDF2-SHA256 and a fresh
// random salt per encryption, so the same file + password never encrypts to
// the same bytes, and the password itself is never stored or derivable from
// the output.
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
const val SALT_LENGTH = 16
const val IV_LENGTH = 12
const val ITERATIONS = 100_000
// GCM appends a 16-byte auth tag to the ciphertext; the smallest possible
// encrypted payload is therefore salt + IV + tag = 44 bytes.
private const val TAG_LENGTH = 16
private const val MIN_LENGTH = SALT_LENGTH + IV_LENGTH + TAG_LENGTH
private val random = SecureRandom()
/** PBKDF2-SHA256 (100k iterations) -> AES-256 key. */
private fun deriveKey(password: String, salt: ByteArray): SecretKeySpec {
val factory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256")
val spec = PBEKeySpec(password.toCharArray(), salt, ITERATIONS, 256)
val keyBytes = factory.generateSecret(spec).encoded
return SecretKeySpec(keyBytes, "AES")
}
/** Encrypt `data` under `password`. Returns salt || IV || ciphertext+tag. */
fun encryptFile(data: ByteArray, password: String): ByteArray {
if (password.isEmpty()) throw IllegalArgumentException("Password must not be empty.")
if (data.isEmpty()) throw IllegalArgumentException("Input data is empty - nothing to encrypt.")
val salt = ByteArray(SALT_LENGTH).also(random::nextBytes)
val iv = ByteArray(IV_LENGTH).also(random::nextBytes)
val key = deriveKey(password, salt)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.ENCRYPT_MODE, key, GCMParameterSpec(TAG_LENGTH * 8, iv))
val ciphertext = cipher.doFinal(data) // ciphertext + 16-byte auth tag
val out = ByteArray(SALT_LENGTH + IV_LENGTH + ciphertext.size)
salt.copyInto(out, 0)
iv.copyInto(out, SALT_LENGTH)
ciphertext.copyInto(out, SALT_LENGTH + IV_LENGTH)
return out
}
/**
* Decrypt a payload produced by [encryptFile]. Throws when the password is
* wrong or the payload was corrupted/tampered (GCM auth-tag failure).
*/
fun decryptFile(data: ByteArray, password: String): ByteArray {
if (password.isEmpty()) throw IllegalArgumentException("Password must not be empty.")
if (data.size < MIN_LENGTH) {
throw IllegalArgumentException(
"Input is too short to be an encrypted file (needs at least $MIN_LENGTH bytes: salt + IV + auth tag)."
)
}
val salt = data.copyOfRange(0, SALT_LENGTH)
val iv = data.copyOfRange(SALT_LENGTH, SALT_LENGTH + IV_LENGTH)
val ciphertext = data.copyOfRange(SALT_LENGTH + IV_LENGTH, data.size)
val key = deriveKey(password, salt)
val cipher = Cipher.getInstance("AES/GCM/NoPadding")
cipher.init(Cipher.DECRYPT_MODE, key, GCMParameterSpec(TAG_LENGTH * 8, iv))
return try {
cipher.doFinal(ciphertext)
} catch (e: Exception) {
// A GCM auth-tag failure means the key did not match (wrong password) or
// the payload was modified after encryption.
throw IllegalArgumentException("Decryption failed: wrong password or corrupted file.", 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 →