SSH Key Generator — Java 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 Java implementation — the same logic the interactive tool runs, in a shareable, citable form.
// SSH Key Generator — Ed25519 / RSA SSH key pair generation.
//
// Language: Java (17+, standard library only)
// Ported from src/lib/ssh-keygen.ts
// display source — part of CosmoDev's polyglot tool pages.
//
// 100% local: keys are created with java.security and never leave the machine.
// Formats produced (matching the TS reference, which uses Web Crypto):
// - 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).
//
// Java's KeyPairGenerator("Ed25519") ships since JDK 15; the raw public key
// and the private seed are extracted from the X.509 SPKI / PKCS#8 encodings
// the same way the TS reference extracts them from Web Crypto exports.
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.SecureRandom;
import java.security.interfaces.RSAPublicKey;
import java.util.Base64;
public final class SshKeygen {
/** Supported algorithms: "ed25519", "rsa-2048", "rsa-4096". */
public record SSHKeyOptions(String algorithm, String comment) {
}
public record SSHKeyPair(String publicKey, String privateKey, String fingerprint) {
}
private static final byte[] MAGIC = "openssh-key-v1\0".getBytes(java.nio.charset.StandardCharsets.US_ASCII);
private static final SecureRandom RANDOM = new SecureRandom();
private static byte[] concat(byte[]... parts) {
int total = 0;
for (byte[] p : parts) total += p.length;
byte[] out = new byte[total];
int o = 0;
for (byte[] p : parts) {
System.arraycopy(p, 0, out, o, p.length);
o += p.length;
}
return out;
}
private static byte[] u32(int n) {
return new byte[] {(byte) (n >>> 24), (byte) (n >>> 16), (byte) (n >>> 8), (byte) n};
}
/** SSH "string": 4-byte big-endian length + raw bytes. */
private static byte[] sshString(byte[] data) {
return concat(u32(data.length), data);
}
private static byte[] sshString(String data) {
return sshString(data.getBytes(java.nio.charset.StandardCharsets.UTF_8));
}
/** SSH "mpint": big-endian integer, minimal, with a leading zero when the
* high bit is set. BigInteger.toByteArray() is already minimal two's
* complement, so only the sign byte handling is needed. */
private static byte[] mpint(BigInteger value) {
byte[] bytes = value.toByteArray();
int i = 0;
while (i < bytes.length - 1 && bytes[i] == 0) i++;
byte[] v = java.util.Arrays.copyOfRange(bytes, i, bytes.length);
return (v.length > 0 && (v[0] & 0x80) != 0)
? sshString(concat(new byte[] {0}, v))
: sshString(v);
}
private static String toBase64(byte[] bytes) {
return Base64.getEncoder().encodeToString(bytes);
}
private static String pemWrap(String base64, String label, int width) {
StringBuilder sb = new StringBuilder("-----BEGIN ").append(label).append("-----\n");
for (int i = 0; i < base64.length(); i += width) {
sb.append(base64, i, Math.min(i + width, base64.length())).append('\n');
}
sb.append("-----END ").append(label).append("-----\n");
return sb.toString();
}
/** "SHA256:<base64 digest of the public blob, unpadded>" - the OpenSSH fingerprint format. */
private static String sshFingerprint(byte[] pubBlob) throws Exception {
byte[] digest = MessageDigest.getInstance("SHA-256").digest(pubBlob);
return "SHA256:" + Base64.getEncoder().withoutPadding().encodeToString(digest);
}
/**
* Build the unencrypted "openssh-key-v1" private key structure for
* Ed25519 (cipher "none", kdf "none"), wrapped in an OPENSSH PRIVATE KEY PEM.
*/
private static String openSshPrivatePem(byte[] pubBlob, byte[] privateKeyBytes, String comment) {
byte[] check = new byte[4];
RANDOM.nextBytes(check);
byte[] 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,...
int padLen = (8 - (inner.length % 8)) % 8;
byte[] pad = new byte[padLen];
for (int i = 0; i < padLen; i++) pad[i] = (byte) (i + 1);
inner = concat(inner, pad);
byte[] outer = concat(
MAGIC,
sshString("none"), // ciphername
sshString("none"), // kdfname
sshString(new byte[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 JDK's crypto. */
public static SSHKeyPair generateSSHKeyPair(SSHKeyOptions options) throws Exception {
String algorithm = options.algorithm();
String comment = options.comment() == null ? "" : options.comment().trim();
String suffix = comment.isEmpty() ? "" : " " + comment;
if ("ed25519".equals(algorithm)) {
KeyPairGenerator kpg = KeyPairGenerator.getInstance("Ed25519");
KeyPair kp = kpg.generateKeyPair();
byte[] spki = kp.getPublic().getEncoded();
// The X.509 SPKI wrapper always ends with the raw 32-byte key.
if (spki.length < 32) {
throw new IllegalStateException("Unexpected Ed25519 public key encoding.");
}
byte[] pub = java.util.Arrays.copyOfRange(spki, spki.length - 32, spki.length);
// The PKCS#8 wrapper is <...header...> 04 20 <32-byte seed>; the seed
// is always the trailing 32 bytes.
byte[] pkcs8 = kp.getPrivate().getEncoded();
if (pkcs8.length < 34) {
throw new IllegalStateException("Unexpected Ed25519 private key encoding.");
}
byte[] seed = java.util.Arrays.copyOfRange(pkcs8, pkcs8.length - 32, pkcs8.length);
byte[] pubBlob = concat(sshString("ssh-ed25519"), sshString(pub));
return new SSHKeyPair(
"ssh-ed25519 " + toBase64(pubBlob) + suffix,
openSshPrivatePem(pubBlob, concat(seed, pub), comment),
sshFingerprint(pubBlob));
}
if ("rsa-2048".equals(algorithm) || "rsa-4096".equals(algorithm)) {
int modulusLength = "rsa-4096".equals(algorithm) ? 4096 : 2048;
KeyPairGenerator kpg = KeyPairGenerator.getInstance("RSA");
kpg.initialize(modulusLength);
KeyPair kp = kpg.generateKeyPair();
RSAPublicKey pub = (RSAPublicKey) kp.getPublic();
byte[] pubBlob = concat(
sshString("ssh-rsa"),
mpint(pub.getPublicExponent()),
mpint(pub.getModulus()));
byte[] pkcs8 = kp.getPrivate().getEncoded();
return new SSHKeyPair(
"ssh-rsa " + toBase64(pubBlob) + suffix,
pemWrap(toBase64(pkcs8), "PRIVATE KEY", 64),
sshFingerprint(pubBlob));
}
// The ECDSA-P256 SSH variant is intentionally not part of this tool's surface.
throw new IllegalArgumentException("Unsupported algorithm: " + algorithm);
}
/** Generate with defaults (no comment). */
public static SSHKeyPair generateSSHKeyPair(String algorithm) throws Exception {
return generateSSHKeyPair(new SSHKeyOptions(algorithm, ""));
}
private SshKeygen() {
}
}
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 →