PGP Key Generator — Kotlin source
Generate PGP key pairs (ECC or RSA) in your browser. Download your public and private keys. Powered by OpenPGP.js.
This is the Kotlin implementation — the same logic the interactive tool runs, in a shareable, citable form.
// PGP Key Generator — ASCII-armored OpenPGP key pair generation.
//
// Language: Kotlin 1.9+ (JVM), BouncyCastle bcpg (org.bouncycastle:bcpg-jdk18on).
// The JVM has no OpenPGP in the standard library; BouncyCastle's openpgp
// package is the canonical implementation (the native counterpart to the TS
// reference's openpgp.js).
// Ported from src/lib/pgp-keygen.ts — display source, part of CosmoDev's
// polyglot tool pages. Functionally equivalent to the TS reference: same
// algorithm choices (Curve25519 ECC / RSA-2048 / RSA-4096), same armored
// outputs (public block, private block, revocation certificate), same v4
// fingerprint. Keys are generated in-process and never leave it.
//
// The TS reference is async because openpgp.js is; the JVM calls are
// synchronous, so this port is too.
import java.io.ByteArrayOutputStream
import java.security.KeyPairGenerator
import java.security.SecureRandom
import java.security.Security
import java.security.interfaces.RSAPublicKey
import java.util.Date
import org.bouncycastle.bcpg.ArmoredOutputStream
import org.bouncycastle.bcpg.PublicKeyAlgorithmTags
import org.bouncycastle.openpgp.PGPEncryptedData
import org.bouncycastle.jce.provider.BouncyCastleProvider
import org.bouncycastle.openpgp.PGPKeyPair
import org.bouncycastle.openpgp.PGPKeyRingGenerator
import org.bouncycastle.openpgp.PGPPublicKey
import org.bouncycastle.openpgp.PGPPublicKeyRing
import org.bouncycastle.openpgp.PGPSecretKeyRing
import org.bouncycastle.openpgp.PGPSignature
import org.bouncycastle.openpgp.PGPSignatureGenerator
import org.bouncycastle.openpgp.PGPSignatureSubpacketGenerator
import org.bouncycastle.openpgp.PGPUtil
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPContentSignerBuilder
import org.bouncycastle.openpgp.operator.jcajce.JcaPGPKeyPair
import org.bouncycastle.openpgp.operator.jcajce.JcePBESecretKeyDecryptorBuilder
import org.bouncycastle.util.encoders.Hex
// JcaJce operator factories need the BC provider registered once.
private val BC_READY = Security.getProvider("BC") == null &&
Security.addProvider(BouncyCastleProvider()) != null
/** Algorithm choices: ECC Curve25519 (default), RSA-2048, or RSA-4096. */
enum class PGPKeyGenAlgorithm { ECC, RSA_2048, RSA_4096 }
data class PGPKeyGenOptions(
/** User's real name (goes into the key's user ID packet). */
val name: String,
/** User's email (goes into the key's user ID packet). */
val email: String,
/** Optional passphrase. If given, the private key is encrypted with it. */
val passphrase: String? = null,
val algorithm: PGPKeyGenAlgorithm,
)
data class PGPKeyPairResult(
/** ASCII-armored public key (BEGIN PGP PUBLIC KEY BLOCK). */
val publicKey: String,
/** ASCII-armored private key (BEGIN PGP PRIVATE KEY BLOCK). */
val privateKey: String,
/** v4 fingerprint, 40 lowercase hex chars, no spaces. */
val fingerprint: String,
/** ASCII-armored revocation certificate. */
val revocationCertificate: String,
)
/** Accepts `foo@bar.tld`-style addresses: one @, non-empty local + domain, a dot in the domain. */
private val EMAIL_RE = Regex("^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$")
/** Validate the identity that goes into the key's user ID. Throws on invalid input. */
fun validateKeyGenIdentity(name: String, email: String) {
if (name.isBlank()) throw IllegalArgumentException("Name is required.")
if (email.isBlank()) throw IllegalArgumentException("Email is required.")
if (!EMAIL_RE.matches(email.trim())) throw IllegalArgumentException("Invalid email address.")
}
private fun armor(bytes: ByteArray, label: String): String {
// ArmoredOutputStream picks the block type from the first packet written
// (secret-key packet -> PRIVATE KEY BLOCK, public-key packet -> PUBLIC
// KEY BLOCK); `label` documents which one the caller expects.
val out = ByteArrayOutputStream()
ArmoredOutputStream(out).use { stream ->
stream.setHeader("Version", "CosmoDev Kotlin")
stream.write(bytes)
}
val text = out.toString(Charsets.UTF_8)
check(text.contains(label)) { "Expected a $label armor block, got: ${text.lineSequence().firstOrNull()}" }
return text
}
/** Fresh RSA key pair of `bits` with the standard F4 exponent. */
private fun rsaKeyPair(bits: Int): java.security.KeyPair =
KeyPairGenerator.getInstance("RSA").apply {
initialize(java.security.spec.RSAKeyGenParameterSpec(bits, java.security.spec.RSAKeyGenParameterSpec.F4))
}.generateKeyPair()
/**
* Generate an ASCII-armored PGP key pair. ECC (Curve25519) is fast; RSA-4096
* can take a few seconds.
*
* Every key gets a signing-capable primary key plus an encryption subkey —
* the same shape openpgp.js produces for type 'curve25519' / 'rsa'.
*/
fun generatePGPKeyPair(options: PGPKeyGenOptions): PGPKeyPairResult {
validateKeyGenIdentity(options.name, options.email)
val userId = "${options.name.trim()} <${options.email.trim()}>"
// An empty-string passphrase would still encrypt the key; only a real
// passphrase should.
val pass = options.passphrase?.takeIf { it.isNotEmpty() }?.toCharArray()
val random = SecureRandom()
val now = Date()
// --- Primary (signing) key + encryption subkey, per algorithm ---
val (master: PGPKeyPair, sub: PGPKeyPair) = when (options.algorithm) {
PGPKeyGenAlgorithm.ECC -> {
// Ed25519 signs, X25519 encrypts — the Curve25519 pair, exactly
// like openpgp.js's type 'curve25519'.
val ed = KeyPairGenerator.getInstance("Ed25519").genKeyPair()
val x = KeyPairGenerator.getInstance("X25519").genKeyPair()
JcaPGPKeyPair(PublicKeyAlgorithmTags.EDDSA_LEGACY, ed, now) to
JcaPGPKeyPair(PGPPublicKey.X25519, x, Date(now.time + 1))
}
PGPKeyGenAlgorithm.RSA_2048, PGPKeyGenAlgorithm.RSA_4096 -> {
val bits = if (options.algorithm == PGPKeyGenAlgorithm.RSA_4096) 4096 else 2048
val sign = rsaKeyPair(bits)
val enc = rsaKeyPair(bits)
JcaPGPKeyPair(PGPPublicKey.RSA_GENERAL, sign, now) to
JcaPGPKeyPair(PGPPublicKey.RSA_ENCRYPT, enc, Date(now.time + 1))
}
}
// --- Self-certification + key flags on the primary key ---
val hashed = PGPSignatureSubpacketGenerator().apply {
setKeyFlags(false, PGPSignature.CERTIFY_OTHER or PGPSignature.SIGN_DATA)
setPreferredSymmetricAlgorithms(
false,
intArrayOf(PGPEncryptedData.AES_256, PGPEncryptedData.AES_192, PGPEncryptedData.AES_128),
)
setPreferredHashAlgorithms(
false,
intArrayOf(PGPUtil.SHA512, PGPUtil.SHA384, PGPUtil.SHA256, PGPUtil.SHA224),
)
setFeature(false, 1) // modification detection (RFC 4880bis feature bit)
}
val ringGenerator = PGPKeyRingGenerator(
PGPSignature.POSITIVE_CERTIFICATION,
master,
userId,
PGPUtil.SHA256,
PGPEncryptedData.AES_256, // secret-key protection
pass ?: CharArray(0),
hashed.generate(),
null,
random,
"BC",
)
// --- Encryption subkey, bound with its own flags ---
val subpackets = PGPSignatureSubpacketGenerator().apply {
setKeyFlags(false, PGPSignature.ENCRYPT_COMMS or PGPSignature.ENCRYPT_STORAGE)
}
ringGenerator.addSubKey(sub, subpackets.generate(), null)
val secretRing: PGPSecretKeyRing = ringGenerator.generateSecretKeyRing()
val publicRing: PGPPublicKeyRing = ringGenerator.generatePublicKeyRing()
// --- Revocation certificate: a key-revocation signature over the public
// ring, armored like openpgp.js's revocationCertificate output. ---
val masterSecret = secretRing.secretKey
val masterPrivate = masterSecret.extractPrivateKey(
JcePBESecretKeyDecryptorBuilder(PGPUtil.SHA1).setProvider("BC").build(pass ?: CharArray(0)),
)
val revoker = PGPSignatureGenerator(
JcaPGPContentSignerBuilder(masterSecret.publicKey.algorithm, PGPUtil.SHA256).setProvider("BC"),
).apply { init(PGPSignature.KEY_REVOCATION, masterPrivate) }
val revokedRing = publicRing.addCertification(publicRing.publicKey, revoker.generate())
return PGPKeyPairResult(
publicKey = armor(publicRing.encoded, "PGP PUBLIC KEY BLOCK"),
privateKey = armor(secretRing.encoded, "PGP PRIVATE KEY BLOCK"),
fingerprint = Hex.toHexString(publicRing.publicKey.fingerprint), // lowercase, like openpgp.js
revocationCertificate = armor(revokedRing.encoded, "PGP PUBLIC KEY BLOCK"),
)
}
/** Key size of an RSA key pair's public half — used by tests to assert the requested bits. */
fun rsaBits(pair: java.security.KeyPair): Int = (pair.public as RSAPublicKey).modulus.bitLength()
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 →