Skip to content

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 →