如何在 Java 中读取 PuTTYgen RSA 密钥



我正在使用PuTTYgen生成RSA密钥并将其导出为OpenSSH格式。然后他们看起来像这样

-----BEGIN RSA PRIVATE KEY-----
Proc-Type: 4,ENCRYPTED
DEK-Info: DES-EDE3-CBC,14EB795C6D5C665D
LIEmjggNbGCPMSR4XaoL3VEEbeG++Lwn/r/GjT0eGJbAaGBqH4jzpZ8ly6ZQ+2Pf
N4uLIsFgK+9a6rfM1cglvQ4IJjsw3740OFcUs4VgX4gOwqZJ6sKrrVIMCFqKfzcd
RsW9q1tFXVKXHO+OYaRY1SjGvQjZMYj1YG22njXFndmZfhC5HPba7UT7oA/p4Whu
XTbQ8r00qNjrdEG6CiK8cX2o6MITe9KITKhF/yc6snrpq222rP7P2CNGhvxLXlCI
K2KjuSHfQ5MBj9ohjEEykcyz+JYZ1E1QSR44JD025k5haC6oOYv/nnBnxAtSB6Vj
J9/ydIJUNpc9XNi3R0dS9D4wBkNUEdXMJTKkBK5zwu145soJ452YWjJ6dI/iYmic
27WRFuMln615QD0CAUUCRMFFQjA4uf/FDR+Im3glHArAWkFRElWOpuk9+ZzbcjYR
2T8ffqB80aTqCYoJ707Qc1l7VwRMBTaAk8ota0Iys2Z9PxqDDh1F+fS27GIbPNLc
2ZV12isUSOSYzyj/5R958vPMJ8ht00lCpDm81t7Yt6M3pBbUDq6QJG7fH9itrl/V
889IA2MZSt39lb/GzvMWRdraoKjw2OFSDu1Cjh7ScvMwzXF/sEI8d3Bf2bkudq/C
5t/h4Cczrlf7PQp85AvsZXL3xyGsFfvdEB0RzTg1andf55boka2ZWggJR9icW+Uh
6l9pAHfDDUaAXCJgVMV9p4IPtZyRp/OAd3vpBo5JrbpxFzoUDIIkkhhSQrANm6Hp
CtZJtQyR5OTHZUT0s3gqi+M9cgbnSEZjDPTuUzEorZWBa1fUX3+FsIMKAI7yNP8j
ALPLStDDtUJZVUtXjNdSZWH5qgsSa3xQq2a1iiaURKOMcDyELQgZbCrr+h78GSBA
XS9b3i5XkAA2g0UcVlmjY0Rm4gMbb2IUqVqGfpgs09ql60O2f6aTypz54ZvVz2f4
XrIGhlxabf+q8u6PFJNUJt33TDMSg1Y1epiu8gVngGqYIwgEJZusWYKSlS0zyzeB
FTVSFJ/7KbCmwRMQnV6gYQA5tIy1ka3Z0SRfbWH8Gu37HTd58RJmQhqXIbvZ+y9e
Irnc/mkkvsi5aaqj64/VyG1rejnssvztFMss7NjHQMr5B9exnfPaw15hU+iXnYGC
2taMxNTlxWOPW+ZTkNpCy2a2fz7URLTVRvk3U2IUqCNi4WqGxZRtJhGphdVnc6Jx
rX7KuvM2SOH0ZhxYiqlb/f0KjTEZ1XOUox/gAVmF015MFHurT66W3G8CB+zP9TVG
7DVQQRwWmSzxZOHC3+3Net2bRMoqpsdqXPRWllLBlby8f0nJMmciSbcyKYZ9lmPp
RNYuGCEKCzPNTtW0sxFKpY57odum8i7n+cW7OJbQ6ItxcEkxSvXybHOkUTXDaZK9
4qkl7Vnaw1YzGyWCW1B47XkrqFhc0Kt1XQHQsbfS9qDvt0Kh72FQLfvaJvTKLnI1
6YoEIP+x0NsbkvjfRvArfs/cdlngg1SMT18UstFqAgbVppnppGRfWti+neHRWsoj
SzvG+oJa72TMCdIekAhp1oLKDFM+jkb7CAZu+ocSpwjST4XJ0rhefA==
-----END RSA PRIVATE KEY-----

这只是一个带有密码短语"test123"(不带引号)的任意 SSH2-RSA 密钥。

当存储为文件时,如何在 Java 中读取此信息以将其用作

RSAPublicKey

RSAPrivateKey

请尽量详细,我知道密码学的基础知识,但我远非专家......

您必须解码OpenSSH密钥,结果将是一个KeyPair

try {
 String PEMPrivateKey = "...";
 String password = "";
 java.security.KeyPair key = PEMDecoder.decodeKeyPairOpenSSH(PEMPrivateKey, password);
 java.security.interfaces.RSAPublicKey pub = (java.security.interfaces.RSAPublicKey)key.getPublic();
} catch (IOException e) {
 // key not in new OpenSSH key format
}

下面是来自trilead SSH项目的PEM解码器。

import java.io.BufferedReader;
import java.io.CharArrayReader;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyPair;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.spec.InvalidKeySpecException;
import com.trilead.ssh2.crypto.cipher.AES;
import com.trilead.ssh2.crypto.cipher.BlockCipher;
import com.trilead.ssh2.crypto.cipher.CBCMode;
import com.trilead.ssh2.crypto.cipher.DES;
import com.trilead.ssh2.crypto.cipher.DESede;
import com.trilead.ssh2.crypto.digest.MD5;
import com.trilead.ssh2.signature.DSAPrivateKey;
import com.trilead.ssh2.signature.DidECKeyPair;
import com.trilead.ssh2.signature.ECDSAKeyAlgorithm;
import com.trilead.ssh2.signature.KeyAlgorithm;
import com.trilead.ssh2.signature.KeyAlgorithmManager;
import com.trilead.ssh2.signature.RSAPrivateKey;
import org.bouncycastle.asn1.ASN1ObjectIdentifier;
import org.bouncycastle.asn1.x9.ECNamedCurveTable;
import org.bouncycastle.asn1.x9.X9ECParameters;
import org.bouncycastle.jcajce.provider.asymmetric.ec.KeyFactorySpi;
import org.bouncycastle.jce.spec.ECPrivateKeySpec;
import org.bouncycastle.jce.spec.ECPublicKeySpec;
import org.bouncycastle.math.ec.ECPoint;
/**
 * PEM Support.
 * 
 * @author Christian Plattner, plattner@trilead.com
 * @version $Id: PEMDecoder.java,v 1.2 2008/04/01 12:38:09 cplattne Exp $
 */
public class PEMDecoder
{
    private static final int PEM_RSA_PRIVATE_KEY = 1;
    private static final int PEM_DSA_PRIVATE_KEY = 2;
    private static final int PEM_EC_PRIVATE_KEY = 3;
    private static final int hexToInt(char c)
    {
        if ((c >= 'a') && (c <= 'f'))
        {
            return (c - 'a') + 10;
        }
        if ((c >= 'A') && (c <= 'F'))
        {
            return (c - 'A') + 10;
        }
        if ((c >= '0') && (c <= '9'))
        {
            return (c - '0');
        }
        throw new IllegalArgumentException("Need hex char");
    }
    private static byte[] hexToByteArray(String hex)
    {
        if (hex == null)
            throw new IllegalArgumentException("null argument");
        if ((hex.length() % 2) != 0)
            throw new IllegalArgumentException("Uneven string length in hex encoding.");
        byte decoded[] = new byte[hex.length() / 2];
        for (int i = 0; i < decoded.length; i++)
        {
            int hi = hexToInt(hex.charAt(i * 2));
            int lo = hexToInt(hex.charAt((i * 2) + 1));
            decoded[i] = (byte) (hi * 16 + lo);
        }
        return decoded;
    }
    private static byte[] generateKeyFromPasswordSaltWithMD5(byte[] password, byte[] salt, int keyLen)
            throws IOException
    {
        if (salt.length < 8)
            throw new IllegalArgumentException("Salt needs to be at least 8 bytes for key generation.");
        MD5 md5 = new MD5();
        byte[] key = new byte[keyLen];
        byte[] tmp = new byte[md5.getDigestLength()];
        while (true)
        {
            md5.update(password, 0, password.length);
            md5.update(salt, 0, 8); // ARGH we only use the first 8 bytes of the
            // salt in this step.
            // This took me two hours until I got AES-xxx running.
            int copy = (keyLen < tmp.length) ? keyLen : tmp.length;
            md5.digest(tmp, 0);
            System.arraycopy(tmp, 0, key, key.length - keyLen, copy);
            keyLen -= copy;
            if (keyLen == 0)
                return key;
            md5.update(tmp, 0, tmp.length);
        }
    }
    private static byte[] removePadding(byte[] buff, int blockSize) throws IOException
    {
        /* Removes RFC 1423/PKCS #7 padding */
        int rfc_1423_padding = buff[buff.length - 1] & 0xff;
        if ((rfc_1423_padding < 1) || (rfc_1423_padding > blockSize))
            throw new IOException("Decrypted PEM has wrong padding, did you specify the correct password?");
        for (int i = 2; i <= rfc_1423_padding; i++)
        {
            if (buff[buff.length - i] != rfc_1423_padding)
                throw new IOException("Decrypted PEM has wrong padding, did you specify the correct password?");
        }
        byte[] tmp = new byte[buff.length - rfc_1423_padding];
        System.arraycopy(buff, 0, tmp, 0, buff.length - rfc_1423_padding);
        return tmp;
    }
    private static final PEMStructure parsePEM(char[] pem) throws IOException
    {       
        PEMStructure ps = new PEMStructure();
        String line = null;
        BufferedReader br = new BufferedReader(new CharArrayReader(pem));
        String endLine = null;
        while (true)
        {
            line = br.readLine();
            if (line == null)
                throw new IOException("Invalid PEM structure, '-----BEGIN...' missing");
            line = line.trim();
            if (line.startsWith("-----BEGIN DSA PRIVATE KEY-----"))
            {
                endLine = "-----END DSA PRIVATE KEY-----";
                ps.pemType = PEM_DSA_PRIVATE_KEY;
                break;
            }
            if (line.startsWith("-----BEGIN RSA PRIVATE KEY-----"))
            {
                endLine = "-----END RSA PRIVATE KEY-----";
                ps.pemType = PEM_RSA_PRIVATE_KEY;
                break;
            }
            
            if (line.startsWith("-----BEGIN EC PRIVATE KEY-----"))
            {
                endLine = "-----END EC PRIVATE KEY-----";
                ps.pemType = PEM_EC_PRIVATE_KEY;
                break;
            }           
        }
        while (true)
        {
            line = br.readLine();
            if (line == null)
                throw new IOException("Invalid PEM structure, " + endLine + " missing");
            line = line.trim();
            int sem_idx = line.indexOf(':');
            if (sem_idx == -1)
                break;
            String name = line.substring(0, sem_idx + 1);
            String value = line.substring(sem_idx + 1);
            String values[] = value.split(",");
            for (int i = 0; i < values.length; i++)
                values[i] = values[i].trim();
            // Proc-Type: 4,ENCRYPTED
            // DEK-Info: DES-EDE3-CBC,579B6BE3E5C60483
            if ("Proc-Type:".equals(name))
            {
                ps.procType = values;
                continue;
            }
            if ("DEK-Info:".equals(name))
            {
                ps.dekInfo = values;
                continue;
            }
            /* Ignore line */
        }
        StringBuffer keyData = new StringBuffer();
        while (true)
        {
            if (line == null)
                throw new IOException("Invalid PEM structure , " + new String(pem) + endLine + " missing");
            line = line.trim();
            if (line.startsWith(endLine))
                break;
            keyData.append(line);
            line = br.readLine();
        }
        char[] pem_chars = new char[keyData.length()];
        keyData.getChars(0, pem_chars.length, pem_chars, 0);
        ps.data = Base64.decode(pem_chars);
        if (ps.data.length == 0)
            throw new IOException("Invalid PEM structure, no data available");
        return ps;
    }
    private static PEMStructure parsePEM(char[] pem, CertificateDecoder certificateDecoder) throws IOException
    {
        PEMStructure ps = new PEMStructure();
        String line;
        BufferedReader br = new BufferedReader(new CharArrayReader(pem));
        String endLine;
        while (true)
        {
            line = br.readLine();
            if (line == null)
                throw new IOException("Invalid PEM structure, '-----BEGIN...' missing");
            line = line.trim();
            if (line.startsWith(certificateDecoder.getStartLine()))
            {
                endLine = certificateDecoder.getEndLine();
                break;
            }
        }
        while (true)
        {
            line = br.readLine();
            if (line == null)
                throw new IOException("Invalid PEM structure, " + endLine + " missing");
            line = line.trim();
            int sem_idx = line.indexOf(':');
            if (sem_idx == -1)
                break;
            String name = line.substring(0, sem_idx + 1);
            String value = line.substring(sem_idx + 1);
            String values[] = value.split(",");
            for (int i = 0; i < values.length; i++)
                values[i] = values[i].trim();
            // Proc-Type: 4,ENCRYPTED
            // DEK-Info: DES-EDE3-CBC,579B6BE3E5C60483
            if ("Proc-Type:".equals(name))
            {
                ps.procType = values;
                continue;
            }
            if ("DEK-Info:".equals(name))
            {
                ps.dekInfo = values;
                continue;
            }
            /* Ignore line */
        }
        StringBuilder keyData = new StringBuilder();
        while (true)
        {
            if (line == null)
                throw new IOException("Invalid PEM structure, " + endLine + " missing");
            line = line.trim();
            if (line.startsWith(endLine))
                break;
            keyData.append(line);
            line = br.readLine();
        }
        char[] pem_chars = new char[keyData.length()];
        keyData.getChars(0, pem_chars.length, pem_chars, 0);
        ps.data = Base64.decode(pem_chars);
        if (ps.data.length == 0)
            throw new IOException("Invalid PEM structure, no data available");
        return ps;
    }
    
    private static final void decryptPEM(PEMStructure ps, byte[] pw) throws IOException
    {
        if (ps.dekInfo == null)
            throw new IOException("Broken PEM, no mode and salt given, but encryption enabled");
        if (ps.dekInfo.length != 2)
            throw new IOException("Broken PEM, DEK-Info is incomplete!");
        String algo = ps.dekInfo[0];
        byte[] salt = hexToByteArray(ps.dekInfo[1]);
        BlockCipher bc = null;
        if (algo.equals("DES-EDE3-CBC"))
        {
            DESede des3 = new DESede();
            des3.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 24));
            bc = new CBCMode(des3, salt, false);
        }
        else if (algo.equals("DES-CBC"))
        {
            DES des = new DES();
            des.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 8));
            bc = new CBCMode(des, salt, false);
        }
        else if (algo.equals("AES-128-CBC"))
        {
            AES aes = new AES();
            aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 16));
            bc = new CBCMode(aes, salt, false);
        }
        else if (algo.equals("AES-192-CBC"))
        {
            AES aes = new AES();
            aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 24));
            bc = new CBCMode(aes, salt, false);
        }
        else if (algo.equals("AES-256-CBC"))
        {
            AES aes = new AES();
            aes.init(false, generateKeyFromPasswordSaltWithMD5(pw, salt, 32));
            bc = new CBCMode(aes, salt, false);
        }
        else
        {
            throw new IOException("Cannot decrypt PEM structure, unknown cipher " + algo);
        }
        if ((ps.data.length % bc.getBlockSize()) != 0)
            throw new IOException("Invalid PEM structure, size of encrypted block is not a multiple of "
                    + bc.getBlockSize());
        /* Now decrypt the content */
        byte[] dz = new byte[ps.data.length];
        for (int i = 0; i < ps.data.length / bc.getBlockSize(); i++)
        {
            bc.transformBlock(ps.data, i * bc.getBlockSize(), dz, i * bc.getBlockSize());
        }
        /* Now check and remove RFC 1423/PKCS #7 padding */
        dz = removePadding(dz, bc.getBlockSize());
        ps.data = dz;
        ps.dekInfo = null;
        ps.procType = null;
    }
    public static final boolean isPEMEncrypted(PEMStructure ps) throws IOException
    {
        if (ps.procType == null)
            return false;
        if (ps.procType.length != 2)
            throw new IOException("Unknown Proc-Type field.");
        if ("4".equals(ps.procType[0]) == false)
            throw new IOException("Unknown Proc-Type field (" + ps.procType[0] + ")");
        if ("ENCRYPTED".equals(ps.procType[1]))
            return true;
        return false;
    }
    public static Object decode(char[] pem, String password) throws IOException
    {
        PEMStructure ps = parsePEM(pem);
        if (isPEMEncrypted(ps))
        {
            if (password == null)
                throw new IOException("PEM is encrypted, but no password was specified");
            decryptPEM(ps, password.getBytes("ISO-8859-1"));
        }
        if (ps.pemType == PEM_DSA_PRIVATE_KEY)
        {
            SimpleDERReader dr = new SimpleDERReader(ps.data);
            byte[] seq = dr.readSequenceAsByteArray();
            if (dr.available() != 0)
                throw new IOException("Padding in DSA PRIVATE KEY DER stream.");
            dr.resetInput(seq);
            BigInteger version = dr.readInt();
            if (version.compareTo(BigInteger.ZERO) != 0)
                throw new IOException("Wrong version (" + version + ") in DSA PRIVATE KEY DER stream.");
            BigInteger p = dr.readInt();
            BigInteger q = dr.readInt();
            BigInteger g = dr.readInt();
            BigInteger y = dr.readInt();
            BigInteger x = dr.readInt();
            if (dr.available() != 0)
                throw new IOException("Padding in DSA PRIVATE KEY DER stream.");
            return new DSAPrivateKey(p, q, g, y, x);
        }
        if (ps.pemType == PEM_RSA_PRIVATE_KEY)
        {
            SimpleDERReader dr = new SimpleDERReader(ps.data);
            byte[] seq = dr.readSequenceAsByteArray();
            if (dr.available() != 0)
                throw new IOException("Padding in RSA PRIVATE KEY DER stream.");
            dr.resetInput(seq);
            BigInteger version = dr.readInt();
            if ((version.compareTo(BigInteger.ZERO) != 0) && (version.compareTo(BigInteger.ONE) != 0))
                throw new IOException("Wrong version (" + version + ") in RSA PRIVATE KEY DER stream.");
            BigInteger n = dr.readInt();
            BigInteger e = dr.readInt();
            BigInteger d = dr.readInt();
            return new RSAPrivateKey(d, e, n);
        }
        if (ps.pemType == PEM_EC_PRIVATE_KEY)
        {
            SimpleDERReader DERderReader = new SimpleDERReader(ps.data);
            byte[] sequence = DERderReader.readSequenceAsByteArray();
            if (DERderReader.available() != 0) {
                throw new IOException("Unexpected padding in EC private key");
            }
            SimpleDERReader sequenceReader = new SimpleDERReader(sequence);
            
            BigInteger version = sequenceReader.readInt();
            if ((version.compareTo(BigInteger.ONE) != 0)) {
                throw new IOException("Unexpected version number in EC private key: " + version);
            }
            byte[] privateBytes = sequenceReader.readOctetString();
            String curveOid = null;
            byte[] publicBytes = null;
            while (sequenceReader.available() > 0) {
                int type = sequenceReader.readConstructedType();
                SimpleDERReader fieldReader = sequenceReader.readConstructed();
                switch (type) {
                    case 0:
                        curveOid = fieldReader.readOid();
                        break;
                    case 1:
                        publicBytes = fieldReader.readOctetString();
                        break;
                }
            }
            ASN1ObjectIdentifier asn1 = new ASN1ObjectIdentifier(curveOid);
            X9ECParameters x9 = ECNamedCurveTable.getByOID(asn1);
            
            BigInteger s = new BigInteger(1, privateBytes);
            byte[] publicBytesSlice = new byte[publicBytes.length - 1];
            System.arraycopy(publicBytes, 1, publicBytesSlice, 0, publicBytesSlice.length);
            ECPoint w = ECDSAKeyAlgorithm.decodePoint(publicBytesSlice, x9.getCurve());
            didisoft.sftp.bouncycastle.math.ec.ECCurve curve = x9.getCurve();
            ECPrivateKeySpec privSpec = new ECPrivateKeySpec(s, new didisoft.sftp.bouncycastle.jce.spec.ECParameterSpec(curve, x9.getG(), x9.getN()));
            ECPublicKeySpec pubSpec = new ECPublicKeySpec(w, new didisoft.sftp.bouncycastle.jce.spec.ECParameterSpec(curve, x9.getG(), x9.getN()));
            try {
                                
                // BC hack because jce-impl KeyFactorySpi.generatePublic doesn't support java.security.spec.ECPublicKeySpec !
                // KeyFactory factory = KeyFactory.getInstance("EC");
                KeyFactorySpi.ECDSA factory = new KeyFactorySpi.ECDSA();
                PublicKey ecPublicKey = factory.engineGeneratePublic(pubSpec);
                PrivateKey ecPrivateKey = factory.engineGeneratePrivate(privSpec);
                
                return new DidECKeyPair(ECNamedCurveTable.getName(asn1), ecPublicKey, ecPrivateKey);
            } catch (InvalidKeySpecException ex) {
                throw new IOException("Could not generate EC key pair: " + ex.getMessage());
            }
        }       
        
        throw new IOException("PEM problem: it is of unknown type");
    }

    public static Object decodeKeyPairOpenSSH(char[] pem, String password) throws IOException
    {
        KeyPair key = decodeKeyPair(pem, password);
        if (key.getPrivate() instanceof java.security.interfaces.RSAPrivateKey) {
            java.security.interfaces.RSAPrivateCrtKey rsa = (java.security.interfaces.RSAPrivateCrtKey)key.getPrivate();
            return new RSAPrivateKey(rsa.getPrivateExponent(), rsa.getPublicExponent(), rsa.getModulus());
        } else if (key.getPrivate() instanceof java.security.interfaces.DSAPrivateKey) {
            java.security.interfaces.DSAPublicKey dsaPub = (java.security.interfaces.DSAPublicKey)key.getPublic();
            java.security.interfaces.DSAPrivateKey dsaPriv = (java.security.interfaces.DSAPrivateKey)key.getPrivate();
            return new DSAPrivateKey(dsaPub.getParams().getP(), 
                    dsaPub.getParams().getQ(), 
                    dsaPub.getParams().getG(), 
                    dsaPub.getY(), 
                    dsaPriv.getX());
        } else {
            return null;
        }       
    }
    
    /**
     * Decodes keys in the new OpenSSH format
     * @param pem
     * @param password
     * @return
     * @throws IOException
     */
    public static KeyPair decodeKeyPair(char[] pem, String password) throws IOException
    {
        for (KeyAlgorithm<PublicKey, PrivateKey> algorithm : KeyAlgorithmManager.getSupportedAlgorithms()) {
            for (CertificateDecoder decoder : algorithm.getCertificateDecoders()) {
                try {
                    PEMStructure ps = parsePEM(pem, decoder);
                    if (isPEMEncrypted(ps)) {
                        if (password == null)
                            throw new IOException("PEM is encrypted, but no password was specified");
                        decryptPEM(ps, password.getBytes("ISO-8859-1"));
                    }
                    return decoder.createKeyPair(ps, password);
                } catch (IOException ex) {
                    //LOGGER.log(Level.FINE, "Could not decode PEM Key using current decoder: " + decoder.getClass().getName(), ex);
                    // we couldn't decode the input, try another decoder
                }
            }
        }
        
        throw new IOException("PEM problem: it is of unknown type");
    }
}

最新更新