SSH Key Generator — Kotlin source
Generate Ed25519 or RSA SSH key pairs in your browser. Copy or download the public and private keys. No server involved.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// SSH Key Generator - Ed25519 / RSA SSH key pair generation via the JVM's
// standard KeyPairGenerator. 100% local: keys are created in-process and
// never leave it.
//
// Language: Kotlin 1.9+ (JVM 15+ for Ed25519), standard library only.
// Ported from src/lib/ssh-keygen.ts — display source, part of CosmoDev's
// polyglot tool pages. Functionally equivalent to the TS reference (which
// drives the browser's Web Crypto): same output formats, byte for byte.
//
// Formats produced:
// - Ed25519 public key : `ssh-ed25519 <base64(blob)> <comment>` where blob is
// the OpenSSH wire format (string "ssh-ed25519" + string 32-byte key).
// - Ed25519 private key: `-----BEGIN OPENSSH PRIVATE KEY-----` PEM containing
// the unencrypted "openssh-key-v1" structure (cipher "none", kdf "none").
// - RSA public key : `ssh-rsa <base64(blob)> <comment>` where blob is
// string "ssh-rsa" + mpint e + mpint n.
// - RSA private key : PKCS#8 PEM (`-----BEGIN PRIVATE KEY-----`).
// - Fingerprint : SHA-256 of the raw public key blob, base64-encoded
// without padding, prefixed "SHA256:" (matches OpenSSH).
import java.security.KeyPairGenerator
import java.security.MessageDigest
import java.security.SecureRandom
import java.security.interfaces.RSAPublicKey
import java.security.spec.RSAKeyGenParameterSpec
import java.util.Base64
/** The three key algorithms the tool offers. */
enum class SSHAlgorithm { ED25519, RSA_2048, RSA_4096 }
data class SSHKeyOptions(
val algorithm: SSHAlgorithm,
/** Optional key comment, conventionally `user@host`. Omitted when empty. */
val comment: String? = null,
)
data class SSHKeyPair(
val publicKey: String,
val privateKey: String,
val fingerprint: String,
)
/** "openssh-key-v1\0" - the magic prefix of the private key structure. */
private val MAGIC = "openssh-key-v1\u0000".toByteArray(Charsets.US_ASCII)
private fun concat(vararg parts: ByteArray): ByteArray {
val out = ByteArray(parts.sumOf { it.size })
var offset = 0
for (p in parts) {
p.copyInto(out, offset)
offset += p.size
}
return out
}
private fun u32(n: Int): ByteArray = byteArrayOf(
(n ushr 24).toByte(), (n ushr 16).toByte(), (n ushr 8).toByte(), n.toByte(),
)
/** SSH "string": 4-byte big-endian length + raw bytes. */
private fun sshString(data: ByteArray): ByteArray = concat(u32(data.size), data)
private fun sshString(data: String): ByteArray = sshString(data.toByteArray(Charsets.UTF_8))
/** SSH "mpint": big-endian integer, minimal, with a leading zero when the high bit is set. */
fun mpint(bytes: ByteArray): ByteArray {
var i = 0
while (i < bytes.size - 1 && bytes[i].toInt() == 0) i++
val v = bytes.copyOfRange(i, bytes.size)
return if (v.isNotEmpty() && (v[0].toInt() and 0x80) != 0) {
sshString(concat(byteArrayOf(0), v))
} else {
sshString(v)
}
}
private fun toBase64(bytes: ByteArray): String = Base64.getEncoder().encodeToString(bytes)
private fun pemWrap(base64: String, label: String, width: Int): String =
"-----BEGIN $label-----\n${base64.chunked(width).joinToString("\n")}\n-----END $label-----\n"
/** "SHA256:<base64 digest of the public blob, unpadded>" - the OpenSSH fingerprint format. */
private fun sshFingerprint(pubBlob: ByteArray): String {
val digest = MessageDigest.getInstance("SHA-256").digest(pubBlob)
return "SHA256:" + toBase64(digest).trimEnd('=')
}
/**
* Build the unencrypted "openssh-key-v1" private key structure for Ed25519
* (cipher "none", kdf "none"), wrapped in an OPENSSH PRIVATE KEY PEM.
*/
private fun openSshPrivatePem(pubBlob: ByteArray, privateKeyBytes: ByteArray, comment: String): String {
val check = u32(SecureRandom().nextInt())
var inner = concat(check, check, pubBlob, sshString(privateKeyBytes), sshString(comment))
// Pad to a multiple of the block size (8 for cipher "none") with 1,2,3,...
val padLen = (8 - inner.size % 8) % 8
if (padLen > 0) {
inner = concat(inner, ByteArray(padLen) { (it + 1).toByte() })
}
val outer = concat(
MAGIC,
sshString("none"), // ciphername
sshString("none"), // kdfname
sshString(ByteArray(0)), // kdfoptions
u32(1), // number of keys
sshString(pubBlob),
sshString(inner),
)
return pemWrap(toBase64(outer), "OPENSSH PRIVATE KEY", 70)
}
/** Generate an SSH key pair in OpenSSH format using the JVM's key generators. */
fun generateSSHKeyPair(options: SSHKeyOptions): SSHKeyPair {
val comment = options.comment?.trim() ?: ""
val suffix = if (comment.isNotEmpty()) " $comment" else ""
return when (options.algorithm) {
SSHAlgorithm.ED25519 -> {
val kp = KeyPairGenerator.getInstance("Ed25519").generateKeyPair()
// The X.509 SubjectPublicKeyInfo wrapper is 44 bytes; the raw key
// is always the trailing 32 (the TS reference's exportKey('raw')).
val pub = kp.public.encoded.copyOfRange(kp.public.encoded.size - 32, kp.public.encoded.size)
if (pub.size != 32) {
throw IllegalStateException("Unexpected Ed25519 public key length: ${pub.size}.")
}
// The PKCS#8 wrapper is <...header...> 04 20 <32-byte seed>; the
// seed is always the trailing 32 bytes.
val pkcs8 = kp.private.encoded
if (pkcs8.size < 34) {
throw IllegalStateException("Unexpected Ed25519 private key encoding.")
}
val seed = pkcs8.copyOfRange(pkcs8.size - 32, pkcs8.size)
val pubBlob = concat(sshString("ssh-ed25519"), sshString(pub))
SSHKeyPair(
publicKey = "ssh-ed25519 ${toBase64(pubBlob)}$suffix",
privateKey = openSshPrivatePem(pubBlob, concat(seed, pub), comment),
fingerprint = sshFingerprint(pubBlob),
)
}
SSHAlgorithm.RSA_2048, SSHAlgorithm.RSA_4096 -> {
val bits = if (options.algorithm == SSHAlgorithm.RSA_4096) 4096 else 2048
val kpg = KeyPairGenerator.getInstance("RSA")
kpg.initialize(RSAKeyGenParameterSpec(bits, RSAKeyGenParameterSpec.F4))
val kp = kpg.generateKeyPair()
val rsaPublic = kp.public as RSAPublicKey
val pubBlob = concat(
sshString("ssh-rsa"),
mpint(rsaPublic.publicExponent.toByteArray()),
mpint(rsaPublic.modulus.toByteArray()),
)
SSHKeyPair(
publicKey = "ssh-rsa ${toBase64(pubBlob)}$suffix",
// private.getEncoded() is already PKCS#8 - same as Web Crypto.
privateKey = pemWrap(toBase64(kp.private.encoded), "PRIVATE KEY", 64),
fingerprint = sshFingerprint(pubBlob),
)
}
}
}
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 →