uk.ac.ed.epcc.webapp.ssh.PublicKeyReaderUtil.java Source code

Java tutorial

Introduction

Here is the source code for uk.ac.ed.epcc.webapp.ssh.PublicKeyReaderUtil.java

Source

//| Copyright - The University of Edinburgh 2015                            |
//|                                                                         |
//| Licensed under the Apache License, Version 2.0 (the "License");         |
//| you may not use this file except in compliance with the License.        |
//| You may obtain a copy of the License at                                 |
//|                                                                         |
//|    http://www.apache.org/licenses/LICENSE-2.0                           |
//|                                                                         |
//| Unless required by applicable law or agreed to in writing, software     |
//| distributed under the License is distributed on an "AS IS" BASIS,       |
//| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.|
//| See the License for the specific language governing permissions and     |
//| limitations under the License.                                          |
package uk.ac.ed.epcc.webapp.ssh;

/*
 * Copyright 2009 The jSVNServe Team
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 *
 * Id:        $Rev$
 * Last Changed:    $Date: 2016/01/26 18:57:04 $
 * Last Changed By: $Author: spb $
 * 
 * Imported into WEBAPP code base. Fixed for dss keys.
 * 
 */

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.RSAKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.RSAPublicKeySpec;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;

import org.apache.commons.codec.binary.Base64;

import uk.ac.ed.epcc.webapp.ssh.PublicKeyReaderUtil.PublicKeyParseException.ErrorCode;

/**
 * The class is a utility class to read OpenSSH or SECSH encoded public key
 * texts.
 *
 * @author jSVNServe Team
 * @version $Id: PublicKeyReaderUtil.java,v 1.10 2016/01/26 18:57:04 spb Exp $
 */
public final class PublicKeyReaderUtil {
    /**
     * Begin marker for the SECSH public key file format.
     *
     * @see #extractSecSHBase64(String)
     */
    private static final String BEGIN_PUB_KEY = "---- BEGIN SSH2 PUBLIC KEY ----";

    /**
     * End marker for the SECSH public key file format.
     *
     * @see #extractSecSHBase64(String)
     */
    private static final String END_PUB_KEY = "---- END SSH2 PUBLIC KEY ----";

    /**
     * Key name of the type of public key for DSA algorithm.
     *
     * @see #load(String)
     */
    private static final String SSH2_DSA_KEY = "ssh-dss";

    /**
     * Key name of the type of public key for RSA algorithm.
     *
     * @see #load(String)
     */
    private static final String SSH2_RSA_KEY = "ssh-rsa";

    /**
     * Default constructor is private so that the public key read utility class
     * could not be instantiated.
     */
    private PublicKeyReaderUtil() {
    }

    /**
     * Decodes given public <code>_key</code> text and returns the related
     * public key instance.
     *
     * @param _key      text key of the encoded public key
     * @return decoded public key
     * @throws PublicKeyParseException if the public key could not be parsed
     *                                 from <code>_key</code>
     * @see PublicKeyParseException.ErrorCode#UNKNOWN_PUBLIC_KEY_FILE_FORMAT
     * @see PublicKeyParseException.ErrorCode#UNKNOWN_PUBLIC_KEY_CERTIFICATE_FORMAT
     */
    public static PublicKey load(String _key) throws PublicKeyParseException {
        final int c = _key.charAt(0);

        final String base64;

        if (c == 's') {
            _key = _key.replaceAll("\\s*\n\\s*", ""); // remove spurious newlines from cut-paste        
            base64 = PublicKeyReaderUtil.extractOpenSSHBase64(_key);
        } else if (c == '-') {
            base64 = PublicKeyReaderUtil.extractSecSHBase64(_key);
        } else {
            throw new PublicKeyParseException(PublicKeyParseException.ErrorCode.UNKNOWN_PUBLIC_KEY_FILE_FORMAT);
        }

        final SSH2DataBuffer buf = new SSH2DataBuffer(Base64.decodeBase64(base64.getBytes()));
        final String type = buf.readString();
        final PublicKey ret;
        if (PublicKeyReaderUtil.SSH2_DSA_KEY.equals(type)) {
            ret = decodeDSAPublicKey(buf);
        } else if (PublicKeyReaderUtil.SSH2_RSA_KEY.equals(type)) {
            ret = decodePublicKey(buf);
        } else {
            throw new PublicKeyParseException(
                    PublicKeyParseException.ErrorCode.UNKNOWN_PUBLIC_KEY_CERTIFICATE_FORMAT);
        }

        return ret;
    }

    /** format a key (of the types supported by the {@link #load(String)} method. 
     * 
     * @param key
     * @return public key string
     * @throws PublicKeyParseException 
     * @throws IOException 
     */
    public static String format(PublicKey key) throws PublicKeyParseException, IOException {
        StringBuilder sb = new StringBuilder();
        String alg = key.getAlgorithm();
        if (alg.equalsIgnoreCase("RSA")) {
            RSAPublicKey pub = (RSAPublicKey) key;
            sb.append(SSH2_RSA_KEY);
            sb.append(" ");
            SSH2ByteBuffer buf = new SSH2ByteBuffer();
            buf.writeString(SSH2_RSA_KEY);
            buf.writeMPint(pub.getPublicExponent());
            buf.writeMPint(pub.getModulus());
            sb.append(Base64.encodeBase64String(buf.toByteArray()));
        } else if (alg.equalsIgnoreCase("DSA")) {
            DSAPublicKey pub = (DSAPublicKey) key;
            sb.append(SSH2_DSA_KEY);
            sb.append(" ");
            SSH2ByteBuffer buf = new SSH2ByteBuffer();
            buf.writeString(SSH2_DSA_KEY);
            DSAParams params = pub.getParams();
            buf.writeMPint(params.getP());
            buf.writeMPint(params.getQ());
            buf.writeMPint(params.getG());
            buf.writeMPint(pub.getY());
            sb.append(Base64.encodeBase64String(buf.toByteArray()));
        } else {
            throw new PublicKeyParseException(ErrorCode.UNKNOWN_PUBLIC_KEY_FILE_FORMAT);
        }
        return sb.toString();
    }

    public static String normalise(String key) throws PublicKeyParseException, IOException {
        if (key == null || key.trim().length() == 0) {
            return key;
        }
        return format(load(key));
    }

    /**
     * <p>Extracts from the OpenSSH public key format the base64 encoded SSH
     * public key.</p>
     * <p>An example of such a definition is:<br/>
     * <code>ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAIEA1on8gxCGJJWSRT4uOrR130....</code>
     * </p>
     *
     * @param _key      text of the public key defined in the OpenSSH format
     * @return base64 encoded public-key data
     * @throws PublicKeyParseException if the OpenSSH public key string is
     *                                 corrupt
     * @see PublicKeyParseException.ErrorCode#CORRUPT_OPENSSH_PUBLIC_KEY_STRING
     * @see <a href="http://www.openssh.org">OpenSSH</a>
     */
    public static String extractOpenSSHBase64(final String _key) throws PublicKeyParseException {
        final String base64;
        try {
            final StringTokenizer st = new StringTokenizer(_key);
            st.nextToken();
            base64 = st.nextToken();
        } catch (final NoSuchElementException e) {
            throw new PublicKeyParseException(PublicKeyParseException.ErrorCode.CORRUPT_OPENSSH_PUBLIC_KEY_STRING);
        }

        return base64;
    }

    /**
     * <p>Extracts from the SECSSH public key format the base64 encoded SSH
     * public key.</p>
     * <p>An example of such a definition is:
     * <pre>
     *  ---- BEGIN SSH2 PUBLIC KEY ----
     * Comment: This is my public key for use on \
     * servers which I don't like.
     * AAAAB3NzaC1kc3MAAACBAPY8ZOHY2yFSJA6XYC9HRwNHxaehvx5wOJ0rzZdzoSOXxbET
     * W6ToHv8D1UJ/z+zHo9Fiko5XybZnDIaBDHtblQ+Yp7StxyltHnXF1YLfKD1G4T6JYrdH
     * YI14Om1eg9e4NnCRleaqoZPF3UGfZia6bXrGTQf3gJq2e7Yisk/gF+1VAAAAFQDb8D5c
     * vwHWTZDPfX0D2s9Rd7NBvQAAAIEAlN92+Bb7D4KLYk3IwRbXblwXdkPggA4pfdtW9vGf
     * J0/RHd+NjB4eo1D+0dix6tXwYGN7PKS5R/FXPNwxHPapcj9uL1Jn2AWQ2dsknf+i/FAA
     * vioUPkmdMc0zuWoSOEsSNhVDtX3WdvVcGcBq9cetzrtOKWOocJmJ80qadxTRHtUAAACB
     * AN7CY+KKv1gHpRzFwdQm7HK9bb1LAo2KwaoXnadFgeptNBQeSXG1vO+JsvphVMBJc9HS
     * n24VYtYtsMu74qXviYjziVucWKjjKEb11juqnF0GDlB3VVmxHLmxnAz643WK42Z7dLM5
     * sY29ouezv4Xz2PuMch5VGPP+CDqzCM4loWgV
     * ---- END SSH2 PUBLIC KEY ----
     * </pre></p>
     *
     * @param _key      text of the public key defined in the SECSH format
     * @return base64 encoded public-key data
     * @throws PublicKeyParseException if the SECSSH key text file is corrupt
     * @see PublicKeyParseException.ErrorCode#CORRUPT_SECSSH_PUBLIC_KEY_STRING
     * @see <a href="http://tools.ietf.org/html/draft-ietf-secsh-publickeyfile">IETF Draft for the SECSH format</a>
     */
    private static String extractSecSHBase64(final String _key) throws PublicKeyParseException {
        final StringBuilder base64Data = new StringBuilder();

        boolean startKey = false;
        boolean startKeyBody = false;
        boolean endKey = false;
        boolean nextLineIsHeader = false;
        for (final String line : _key.split("\n")) {
            final String trimLine = line.trim();
            if (!startKey && trimLine.equals(PublicKeyReaderUtil.BEGIN_PUB_KEY)) {
                startKey = true;
            } else if (startKey) {
                if (trimLine.equals(PublicKeyReaderUtil.END_PUB_KEY)) {
                    endKey = true;
                    break;
                } else if (nextLineIsHeader) {
                    if (!trimLine.endsWith("\\")) {
                        nextLineIsHeader = false;
                    }
                } else if (trimLine.indexOf(':') > 0) {
                    if (startKeyBody) {
                        throw new PublicKeyParseException(
                                PublicKeyParseException.ErrorCode.CORRUPT_SECSSH_PUBLIC_KEY_STRING);
                    } else if (trimLine.endsWith("\\")) {
                        nextLineIsHeader = true;
                    }
                } else {
                    startKeyBody = true;
                    base64Data.append(trimLine);
                }
            }
        }

        if (!endKey) {
            throw new PublicKeyParseException(PublicKeyParseException.ErrorCode.CORRUPT_SECSSH_PUBLIC_KEY_STRING);
        }

        return base64Data.toString();
    }

    /**
     * <p>Decodes a DSA public key according to the SSH standard from the
     * data <code>_buffer</code> based on <b>NIST's FIPS-186</b>. The values of
     * the DSA public key specification are read in the order
     * <ul>
     * <li>prime p</li>
     * <li>sub-prime q</li>
     * <li>base g</li>
     * <li>public key y</li>
     * </ul>
     * With the specification the related DSA public key is generated.</p>
     *
     * @param _buffer   SSH2 data buffer where the type of the key is already
     *                  read
     * @return DSA public key instance
     * @throws PublicKeyParseException if the SSH2 public key blob could not be
     *                                 decoded
     * @see DSAPublicKeySpec
     * @see <a href="http://en.wikipedia.org/wiki/Digital_Signature_Algorithm">Digital Signature Algorithm  on Wikipedia</a>
     * @see <a href="http://tools.ietf.org/html/rfc4253#section-6.6">RFC 4253 Section 6.6</a>
     */
    private static PublicKey decodeDSAPublicKey(final SSH2DataBuffer _buffer) throws PublicKeyParseException {
        final BigInteger p = _buffer.readMPint();
        final BigInteger q = _buffer.readMPint();
        final BigInteger g = _buffer.readMPint();
        final BigInteger y = _buffer.readMPint();

        try {
            final KeyFactory dsaKeyFact = KeyFactory.getInstance("DSA");
            final DSAPublicKeySpec dsaPubSpec = new DSAPublicKeySpec(y, p, q, g);

            return dsaKeyFact.generatePublic(dsaPubSpec);

        } catch (final Exception e) {
            throw new PublicKeyParseException(
                    PublicKeyParseException.ErrorCode.SSH2DSA_ERROR_DECODING_PUBLIC_KEY_BLOB, e);
        }
    }

    /**
     * <p>Decode a RSA public key encoded according to the SSH standard from
     * the data <code>_buffer</code>. The values of the RSA public key
     * specification are read in the order
     * <ul>
     * <li>public exponent</li>
     * <li>modulus</li>
     * </ul>
     * With the specification the related RSA public key is generated.</p>
     *
     * @param _buffer   key / certificate data (certificate or public key
     *                  format identifier is already read)
     * @return RSA public key instance
     * @throws PublicKeyParseException if the SSH2 public key blob could not be
     *                                 decoded
     * @see RSAPublicKeySpec
     * @see <a href="http://en.wikipedia.org/wiki/RSA">RSA on Wikipedia</a>
     * @see <a href="http://tools.ietf.org/html/rfc4253#section-6.6">RFC 4253 Section 6.6</a>
     */
    private static PublicKey decodePublicKey(final SSH2DataBuffer _buffer) throws PublicKeyParseException {
        final BigInteger e = _buffer.readMPint();
        final BigInteger n = _buffer.readMPint();

        try {
            final KeyFactory rsaKeyFact = KeyFactory.getInstance("RSA");
            final RSAPublicKeySpec rsaPubSpec = new RSAPublicKeySpec(n, e);

            return rsaKeyFact.generatePublic(rsaPubSpec);

        } catch (final Exception ex) {
            throw new PublicKeyParseException(
                    PublicKeyParseException.ErrorCode.SSH2RSA_ERROR_DECODING_PUBLIC_KEY_BLOB, ex);
        }
    }

    /**
     * The class is used to read from a SSH data buffer the protocol specific
     * formatting defined in
     * <a href="http://tools.ietf.org/html/rfc4253#section-6.6">RFC 4253
     * Section 6.6</a>.
     *
     * @see <a href="http://tools.ietf.org/html/rfc4253#section-6.6">RFC 4253 Section 6.6</a>
     */
    private static class SSH2DataBuffer {
        /**
         * SSH2 data.
         */
        private final byte[] data;

        /**
         * Current position in {@link #data}.
         */
        private int pos;

        /**
         * Initialize the SSH2 data buffer.
         *
         * @param _data binaray data blob
         * @see #data
         */
        public SSH2DataBuffer(final byte[] _data) {
            this.data = _data;
        }

        /**
         * Reads a big integer from {@link #data} starting with {@link #pos}. A
         * big integer is stored as byte array (see {@link #readByteArray()}).
         *
         * @return read big integer
         * @throws PublicKeyParseException if the byte array holds not enough
         *                                 bytes
         * @see #readByteArray()
         */
        public BigInteger readMPint() throws PublicKeyParseException {
            final byte[] raw = this.readByteArray();
            return (raw.length > 0) ? new BigInteger(raw) : BigInteger.valueOf(0);
        }

        /**
         * Reads a string from {@link #data} starting with {@link #pos}. A
         * string is stored as byte array (see {@link #readByteArray()}) in
         * UTF8 format.
         *
         * @return read string
         * @throws PublicKeyParseException if the byte array holds not enough
         *                                 bytes
         * @see #readByteArray()
         */
        public String readString() throws PublicKeyParseException {
            return new String(this.readByteArray());
        }

        /**
         * Reads from the {@link #data} starting with {@link #pos} the next
         * four bytes and prepares an integer.
         *
         * @return 32 bit integer value
         */
        private int readUInt32() {
            final int byte1 = 0xff & this.data[this.pos++];
            final int byte2 = 0xff & this.data[this.pos++];
            final int byte3 = 0xff & this.data[this.pos++];
            final int byte4 = 0xff & this.data[this.pos++];
            return ((byte1 << 24) + (byte2 << 16) + (byte3 << 8) + (byte4 << 0));
        }

        /**
         * Reads from the {@link #data} starting with {@link #pos} a byte
         * array. The byte array is defined as:
         * <ul>
         * <li>first the length of the byte array is defined as integer
         *     (see {@link #readUInt32()})</li>
         * <li>then the byte array itself is defined</li>
         * </ul>
         *
         * @return read byte array from {@link #data}
         * @throws PublicKeyParseException if the byte array holds not enough
         *                                 bytes
         * @see #readUInt32()
         * @see PublicKeyParseException.ErrorCode#CORRUPT_BYTE_ARRAY_ON_READ
         */
        private byte[] readByteArray() throws PublicKeyParseException {
            final int len = this.readUInt32();
            if ((len < 0) || (len > (this.data.length - this.pos))) {
                throw new PublicKeyParseException(PublicKeyParseException.ErrorCode.CORRUPT_BYTE_ARRAY_ON_READ);
            }
            final byte[] str = new byte[len];
            System.arraycopy(this.data, this.pos, str, 0, len);
            this.pos += len;
            return str;
        }
    }

    private static class SSH2ByteBuffer extends ByteArrayOutputStream {
        public void writeMPint(BigInteger i) throws IOException {
            writeByteArray(i.toByteArray());
        }

        public void writeString(String s) throws IOException {
            writeByteArray(s.getBytes());
        }

        private void writeUint32(int i) {
            write((i >> 24) & 0xff);
            write((i >> 16) & 0xff);
            write((i >> 8) & 0xff);
            write(i & 0xff);
        }

        private void writeByteArray(byte data[]) throws IOException {
            writeUint32(data.length);
            write(data);
        }
    }

    /**
     * The Exception is throws if the public key encoded text could not be
     * parsed. For the related {@link PublicKeyParseException#errorCode} see
     * enumeration {@link PublicKeyParseException.ErrorCode}.
     */
    public static final class PublicKeyParseException extends Exception {
        /**
         * Defines the serialize version unique identifier.
         */
        private static final long serialVersionUID = 1446034172449421912L;

        /**
         * Error code of the public key parse exception.
         */
        private final ErrorCode errorCode;

        /**
         * Creates a new exception for defined <code>_errorCode</code>.
         *
         * @param _errorCode    error code
         */
        private PublicKeyParseException(final ErrorCode _errorCode) {
            super(_errorCode.message);
            this.errorCode = _errorCode;
        }

        /**
         * Creates a new exception for defined <code>_errorCode</code> and
         * <code>_cause</code>.
         *
         * @param _errorCode    error code
         * @param _cause        throwable clause
         */
        private PublicKeyParseException(final ErrorCode _errorCode, final Throwable _cause) {
            super(_errorCode.message, _cause);
            this.errorCode = _errorCode;
        }

        /**
         * Returns the error code enumeration of this public key parse
         * exception instance.
         *
         * @return error code of the public key parse exception instance
         * @see #errorCode
         */
        public ErrorCode getErrorCode() {
            return this.errorCode;
        }

        /**
         * Enumeration of the error codes if the public key could not parsed.
         */
        public enum ErrorCode {
            /**
             * The format of the given ASCII key is not known and could not be
             * parsed. Only OpenSSH (starts with 's') and SECSH (starts with
             * '-') are currently supported.
             *
             * @see PublicKeyReaderUtil#load(String)
             */
            UNKNOWN_PUBLIC_KEY_FILE_FORMAT("Corrupt or unknown public key file format"),

            /**
             * The binary blob of the key definition used a not supported
             * public key certificate format. Only DSA and RSA are currently
             * supported.
             *
             * @see PublicKeyReaderUtil#SSH2_DSA_KEY
             * @see PublicKeyReaderUtil#SSH2_RSA_KEY
             * @see PublicKeyReaderUtil#load(String)
             */
            UNKNOWN_PUBLIC_KEY_CERTIFICATE_FORMAT("Corrupt or unknown public key certificate format"),

            /**
             * The public key string is not defined correctly in OpenSSH
             * format.
             *
             * @see PublicKeyReaderUtil#extractOpenSSHBase64(String)
             */
            CORRUPT_OPENSSH_PUBLIC_KEY_STRING("Corrupt OpenSSH public key string"),

            /**
             * The public key string is not defined correctly in SECSSH
             * format.
             *
             * @see PublicKeyReaderUtil#extractSecSHBase64(String)
             */
            CORRUPT_SECSSH_PUBLIC_KEY_STRING("Corrupt SECSSH public key string"),

            /**
             * The DSA public key blob could not decoded.
             *
             * @see PublicKeyReaderUtil#decodeDSAPublicKey(SSH2DataBuffer)
             */
            SSH2DSA_ERROR_DECODING_PUBLIC_KEY_BLOB("SSH2DSA: error decoding public key blob"),

            /**
             * The RSA public key blob could not decoded.
             *
             * @see PublicKeyReaderUtil#decodeRSAPublicKey(SSH2DataBuffer)
             */
            SSH2RSA_ERROR_DECODING_PUBLIC_KEY_BLOB("SSH2RSA: error decoding public key blob"),

            /**
             * @see PublicKeyReaderUtil.SSH2DataBuffer#readByteArray()
             */
            CORRUPT_BYTE_ARRAY_ON_READ("Corrupt byte array on read");

            /**
             * English message of the error code.
             */
            private final String message;

            /**
             * Constructor used to initialize the error codes with an error
             * message.
             *
             * @param _message  message text of the error code
             */
            ErrorCode(final String _message) {
                this.message = _message;
            }
        }
    }
}