From ea786874cd264241db54f685fc8524285478fa9c Mon Sep 17 00:00:00 2001 From: Erdem A Memisyazici Date: Wed, 27 Mar 2024 13:20:50 -0400 Subject: [PATCH] fix #69-ssh-pubkey-pem --- src/main/java/org/cryptacular/KeyDecoder.java | 26 + .../java/org/cryptacular/asn/ASN1Decoder.java | 16 +- .../asn/OpenSSLPrivateKeyDecoder.java | 3 +- .../org/cryptacular/asn/PublicKeyDecoder.java | 41 +- .../org/cryptacular/io/pem/PemObject.java | 782 ++++++++++++++++++ .../cryptacular/ssh/SSHPublicKeyDecoder.java | 134 +++ .../java/org/cryptacular/util/PemUtil.java | 76 +- .../org/cryptacular/util/KeyPairUtilTest.java | 4 + src/test/resources/keys/ssh2-dsa-pub.pem | 14 + src/test/resources/keys/ssh2-dsa-pub.pub | 1 + src/test/resources/keys/ssh2-rsa-pub.pem | 12 + src/test/resources/keys/ssh2-rsa-pub.pub | 1 + 12 files changed, 1080 insertions(+), 30 deletions(-) create mode 100644 src/main/java/org/cryptacular/KeyDecoder.java create mode 100644 src/main/java/org/cryptacular/io/pem/PemObject.java create mode 100644 src/main/java/org/cryptacular/ssh/SSHPublicKeyDecoder.java create mode 100644 src/test/resources/keys/ssh2-dsa-pub.pem create mode 100644 src/test/resources/keys/ssh2-dsa-pub.pub create mode 100644 src/test/resources/keys/ssh2-rsa-pub.pem create mode 100644 src/test/resources/keys/ssh2-rsa-pub.pub diff --git a/src/main/java/org/cryptacular/KeyDecoder.java b/src/main/java/org/cryptacular/KeyDecoder.java new file mode 100644 index 00000000..6cb8f658 --- /dev/null +++ b/src/main/java/org/cryptacular/KeyDecoder.java @@ -0,0 +1,26 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular; + +/** + * Contract interface for converting encoded bytes to an object. + * + * @param Type of object to produce on decode. + * + * @author Middleware Services + */ +public interface KeyDecoder +{ + + /** + * Produces an object from an encoded representation. + * + * @param encoded data. + * @param args Additional data required to perform decoding. + * + * @return Decoded object. + * + * @throws EncodingException on encoding errors. + */ + T decode(byte[] encoded, Object... args) throws EncodingException; + +} diff --git a/src/main/java/org/cryptacular/asn/ASN1Decoder.java b/src/main/java/org/cryptacular/asn/ASN1Decoder.java index b2a8f2b7..ae5aa48c 100644 --- a/src/main/java/org/cryptacular/asn/ASN1Decoder.java +++ b/src/main/java/org/cryptacular/asn/ASN1Decoder.java @@ -1,7 +1,7 @@ /* See LICENSE for licensing and NOTICE for copyright. */ package org.cryptacular.asn; -import org.cryptacular.EncodingException; +import org.cryptacular.KeyDecoder; /** * Strategy interface for converting encoded ASN.1 bytes to an object. @@ -10,18 +10,6 @@ * * @author Middleware Services */ -public interface ASN1Decoder +public interface ASN1Decoder extends KeyDecoder { - - /** - * Produces an object from an encoded representation. - * - * @param encoded ASN.1 encoded data. - * @param args Additional data required to perform decoding. - * - * @return Decoded object. - * - * @throws EncodingException on encoding errors. - */ - T decode(byte[] encoded, Object... args) throws EncodingException; } diff --git a/src/main/java/org/cryptacular/asn/OpenSSLPrivateKeyDecoder.java b/src/main/java/org/cryptacular/asn/OpenSSLPrivateKeyDecoder.java index 5934d858..639e574c 100644 --- a/src/main/java/org/cryptacular/asn/OpenSSLPrivateKeyDecoder.java +++ b/src/main/java/org/cryptacular/asn/OpenSSLPrivateKeyDecoder.java @@ -18,6 +18,7 @@ import org.bouncycastle.crypto.params.RSAPrivateCrtKeyParameters; import org.bouncycastle.jcajce.provider.asymmetric.util.ECUtil; import org.cryptacular.EncodingException; +import org.cryptacular.io.pem.PemObject; import org.cryptacular.pbe.OpenSSLAlgorithm; import org.cryptacular.pbe.OpenSSLEncryptionScheme; import org.cryptacular.util.ByteUtil; @@ -36,7 +37,7 @@ public class OpenSSLPrivateKeyDecoder extends AbstractPrivateKeyDecoder RFC4716_SPECIFIERS = Collections.unmodifiableSet( + Stream.of( + RFC4716_SPECIFIER_SUBJECT, + RFC4716_SPECIFIER_COMMENT + ).collect(Collectors.toSet()) + ); + + /** + * Headers allowed by RFC 4716. {@link #RFC2440_SPECIFIER_MESSAGEID} SHOULD NOT appear unless it is in a multi-part + * message. + */ + public static final Set RFC2440_SPECIFIERS = Collections.unmodifiableSet( + Stream.of( + RFC2440_SPECIFIER_CHARSET, + RFC2440_SPECIFIER_HASH, + RFC2440_SPECIFIER_MESSAGEID, + RFC2440_SPECIFIER_COMMENT, + RFC2440_SPECIFIER_VERSION + ).collect(Collectors.toSet()) + ); + + /** + * Headers allowed by RFC 1421. + */ + public static final Set RFC1421_SPECIFIERS = Collections.unmodifiableSet( + Stream.of( + RFC1421_SPECIFIER_CRL, + RFC1421_SPECIFIER_KEY_INFO, + RFC1421_SPECIFIER_MIC_INFO, + RFC1421_SPECIFIER_ISSUER_CERTIFICATE, + RFC1421_SPECIFIER_ORIGINATOR_CERTIFICATE, + RFC1421_SPECIFIER_RECIPIENT_ID_SYMMETRIC, + RFC1421_SPECIFIER_RECIPIENT_ID_ASYMMETRIC, + RFC1421_SPECIFIER_ORIGINATOR_ID_SYMMETRIC, + RFC1421_SPECIFIER_ORIGINATOR_ID_ASYMMETRIC, + RFC1421_SPECIFIER_DEK_INFO, + RFC1421_SPECIFIER_PROC_TYPE + ).collect(Collectors.toSet()) + ); + + /** + * {@link Descriptor} for this PEM object which includes the RFC governing its format and other relevant data + * about the data. + */ + private final Descriptor descriptor; + + /** + * Generic constructor for object without headers. + * + * @param descriptorParam Descriptor on the content (i.e. RFC governing PEM format, type etc.) + * @param content The binary content of the object. + */ + private PemObject(final Descriptor descriptorParam, final byte[] content) + { + super(descriptorParam.getType(), content); + this.descriptor = descriptorParam; + } + + + /** + * Generic constructor for object with headers. + * + * @param descriptorParam Descriptor on the content (i.e. RFC governing PEM format, type etc.) + * @param headers A list of PemHeader objects. + * @param content The binary content of the object. + */ + private PemObject(final Descriptor descriptorParam, final List headers, final byte[] content) + { + super(descriptorParam.getType(), headers, content); + this.descriptor = descriptorParam; + assertHeadersValid(headers); + } + + + /** + * Returns descriptor on PEM object + * + * @return {@link Descriptor} + */ + public Descriptor getDescriptor() + { + return descriptor; + } + + + /** + * Throws an exception if the headers are not according to their governing RFC format. + * + * @param headers headers + */ + private void assertHeadersValid(final List headers) + { + switch (this.descriptor.getFormat()) { + case RFC1421: + assertPemHeaderValid(headers, RFC1421_SPECIFIERS, + Format.RFC1421, false, true); + break; + case RFC2440: + assertPemHeaderValid(headers, RFC2440_SPECIFIERS, + Format.RFC2440, false, false); + break; + case RFC4716: + assertPemHeaderValid(headers, RFC4716_SPECIFIERS, + Format.RFC4716, true, false); + break; + case RFC7468: + throw new IllegalArgumentException( + "Headers are not allowed in this PEM format specified (RFC 7468)"); + default: + break; + } + } + + + /** + * Checks to make sure a given list of {@link PemHeader} instances the names specified are valid. + * + * @param headers headers + * @param specifiers Set of allowed specifiers/headers + * @param format RFC format + * @param allowX Allow specifiers/headers starting with X- + * @param disregardX Treat specifiers/headers starting with X- as if they do not + * @throws IllegalArgumentException If header element in the headers list is not valid + */ + private void assertPemHeaderValid(final List headers, final Set specifiers, + final Format format, final boolean allowX, final boolean disregardX) + throws IllegalArgumentException + { + for (final Object header : headers) { + if (!(header instanceof PemHeader)) { + throw new IllegalArgumentException("Headers must be of type PemHeader"); + } + final PemHeader pemHeader = (PemHeader) header; + if (pemHeader == null || pemHeader.getName() == null) { + throw new IllegalArgumentException("Neither a supplied PemHeader nor its name may be null"); + } + final boolean isXHeader = pemHeader.getName().toLowerCase() + .startsWith(RFC4716_SPECIFIER_PRIVATE_BEGIN_MARKER); + final String headerName = disregardX && isXHeader ? + pemHeader.getName().substring(2) : pemHeader.getName(); + if (!specifiers.contains(headerName) && !allowX && isXHeader) { + throw new IllegalArgumentException( + String.format("Invalid header \"%s\" specified in PEM format (%s)", + pemHeader.getName(), format.name())); + } + } + } + + public static class Builder + { + + /** + * Default empty constructor. + * + */ + public Builder() + { + } + + public PemObject build(final Descriptor descriptor, final byte[] content) throws IOException + { + return new PemObject(descriptor, content); + } + + public PemObject build(final Descriptor descriptor, final List headers, final byte[] content) throws IOException + { + return new PemObject(descriptor, headers, content); + } + + public PemObject build(final BufferedReader reader) throws IOException + { + return parseInternal(reader, parseDescriptor(reader)); + } + + + /** + * Reads the contents of the PEM data between the BEGIN and END markers by format specified. + * + * @param reader {@link BufferedReader} reader that contains the data to parse + * @param descriptor Descriptor regarding the PEM encoded format (see {@link Descriptor}) + * @return ExtendedPemObject instance with the data read + * @throws IOException In case of exceptions reading the buffer + * @throws IllegalArgumentException In case of malformed PEM data + */ + private static PemObject parseInternal( + final BufferedReader reader, + final Descriptor descriptor) + throws IOException, IllegalArgumentException + { + final List headers = new ArrayList<>(); + int lineLength = -1; + String line; + final String endMarker = (descriptor.getFormat() == Format.RFC4716 ? + PemObject.RFC4716_ENCAPSULATION_END_MARKER : + PemObject.RFC1421_ENCAPSULATION_END_MARKER) + " " + descriptor.getType(); + final String beginMarker = (descriptor.getFormat() == Format.RFC4716 ? + PemObject.RFC4716_ENCAPSULATION_BEGIN_MARKER : + PemObject.RFC1421_ENCAPSULATION_BEGIN_MARKER) + " " + descriptor.getType(); + final String beginLine = reader.readLine(); + if (!beginLine.startsWith(beginMarker)) { + throw new IllegalArgumentException(String.format("%s not found in \"%s", beginMarker, beginLine)); + } + final StringBuilder base64DataBuilder = new StringBuilder(); + while ((line = reader.readLine()) != null) { + final PemHeader headerLine = parseHeader(reader, line, descriptor.getFormat()); + if (headerLine != null) { + headers.add(headerLine); + continue; + } + lineLength = Math.max(lineLength, line.length()); + if (line.contains(endMarker)) { + break; + } + base64DataBuilder.append(line.trim()); + } + final String b64buffer = base64DataBuilder.toString(); + if (line == null) { + throw new IllegalArgumentException(endMarker + " not found"); + } + assertLineLength(descriptor.getFormat(), lineLength); + if (descriptor.getFormat().equals(Format.RFC7468)) { + return new PemObject(descriptor, CodecUtil.b64(b64buffer)); + } else { + return new PemObject(descriptor, headers, CodecUtil.b64(b64buffer)); + } + } + + + /** + * Reads a header line which takes into account header types from RFC 1421 & RFC 4716 + * + * @param reader {@link BufferedReader} instance instantiated with PEM file data + * @param line Current line read in the buffer + * @param format RFC format governing the PEM file + * @return {@link PemHeader} if a header value pair could be successfully read, otherwise null is returned + * @throws IOException In case of any read errors in the buffer + */ + private static PemHeader parseHeader( + final BufferedReader reader, + final String line, + final Format format) throws IOException + { + if (line.contains(":")) { + final int index = line.indexOf(':'); + String specifier = line.substring(0, index); + String value = line.substring(index + 1); + if (format == Format.RFC4716) { + while (value.endsWith("\\")) { + value = value.substring(0, value.length() - 1); + value += reader.readLine(); + } + } else if (format == Format.RFC1421) { + if (specifier.startsWith("X-")) { + //Remove X- as per RFC 1421 Section 4.6 + specifier = specifier.substring(2); + } + String nextLine = peekNextLine(reader, PemObject.RFC1421_MAX_LINE_LENGTH); + while (nextLine.startsWith(" ")) { + value += reader.readLine().trim(); + nextLine = peekNextLine(reader, PemObject.RFC1421_MAX_LINE_LENGTH); + } + } + value = value.trim(); + if (value.length() >= 2 && value.startsWith("\"") && value.endsWith("\"")) { + //Remove the quotes as suggested by RFC 4716 + value = value.substring(1, value.length() - 1); + } + return new PemHeader(specifier, value); + } else { + return null; + } + } + + + /** + * Throws an exception if the data contains rules restricted by their respective RFCs. + * + * @param format Format which governs this PEM data + * @param maxLineLength maximum length in b64buffer lines prior to concatenation + * @throws IllegalArgumentException In case of a constraint violation + */ + private static void assertLineLength( + final Format format, + final int maxLineLength) throws IllegalArgumentException + { + switch (format) { + case RFC4716: + if (maxLineLength > PemObject.RFC4716_MAX_LINE_LENGTH) { + throw new IllegalArgumentException( + "Malformed RFC 4716 type PEM data (b64 lines longer than maximum allowed length)"); + } + break; + case RFC7468: + if (maxLineLength > PemObject.RFC7468_MAX_LINE_LENGTH) { + throw new IllegalArgumentException( + "Malformed RFC 7468 type PEM data (b64 lines longer than maximum allowed length)"); + } + break; + case RFC1421: + if (maxLineLength > PemObject.RFC1421_MAX_LINE_LENGTH) { + throw new IllegalArgumentException( + "Malformed RFC 1421 type PEM data (b64 lines longer than maximum allowed length)"); + } + break; + default: + break; + } + } + + + /** + * Determines the RFC format governing the PEM file in the reader and constructs a + * {@link Descriptor} accordingly. + * + * @param reader {@link BufferedReader} instance instantiated with PEM file data + * @return Populated ExtendedPemObject instance + * @throws IOException In case of exceptions reading the buffer, or malformed PEM data + */ + private static Descriptor parseDescriptor(final BufferedReader reader) + throws IOException + { + final Format format; + final String explanatoryText = readExplanatoryText(reader); + final String firstPemLine = peekNextLine(reader, PemObject.RFC2440_MAX_LINE_LENGTH); + if (firstPemLine != null) { + final String pemType; + final boolean isRFC4716Markers = + firstPemLine.startsWith(PemObject.RFC4716_ENCAPSULATION_BEGIN_MARKER); + pemType = getPemType(isRFC4716Markers, firstPemLine); + format = getFormat(explanatoryText, pemType, isRFC4716Markers); + return new Descriptor(format, explanatoryText, pemType); + } + return null; + } + + + /** + * Determines the RFC governing the format of the PEM file based of the parameters provided. + * + * @param explanatoryText Explanatory text is only allowed in RFC 7468 + * @param pemType All types begin with PGP in RFC 2440 + * @param isRFC4716Markers It either starts with four dashes (RFC RFC4716) or five (RFC 1421) + * @return Format determined (see {@link Descriptor#getFormat()}) + */ + private static Format getFormat( + final String explanatoryText, final String pemType, final boolean isRFC4716Markers) + { + final Format format; + if (explanatoryText.length() > 0) { + format = Format.RFC7468; + } else if (pemType.startsWith("PGP")) { + format = Format.RFC2440; + } else if (isRFC4716Markers) { + format = Format.RFC4716; + } else { + format = Format.RFC1421; + } + return format; + } + + + /** + * Returns the message type based on the marker format provided. + * + * @param isRFC4716Markers isRFC4716Markers (either four dashes and a space, or five dashes) + * @param firstPemLine First line of the PEM file + * @return PEM type + */ + private static String getPemType(final boolean isRFC4716Markers, final String firstPemLine) + { + if (isRFC4716Markers) { + return firstPemLine.substring(PemObject.RFC4716_ENCAPSULATION_BEGIN_MARKER.length(), + firstPemLine.indexOf(PemObject.RFC4716_ENCAPSULATION_MARKER, + PemObject.RFC4716_ENCAPSULATION_BEGIN_MARKER.length())).trim(); + } + return firstPemLine.substring(PemObject.RFC1421_ENCAPSULATION_BEGIN_MARKER.length(), + firstPemLine.indexOf(PemObject.RFC1421_ENCAPSULATION_MARKER, + PemObject.RFC1421_ENCAPSULATION_BEGIN_MARKER.length())).trim(); + } + + + /** + * Reads the explanatory text as described by RFC 7468 5.2. Method simply reads a line until a known header marker + * is found. + * + * @param reader only the explanatory text will actually be read off the reader + * @return Explanatory text + * @throws IOException In case of errors reading the buffer + */ + private static String readExplanatoryText(final BufferedReader reader) throws IOException + { + final StringBuilder explanatoryTextBuilder = new StringBuilder(0); + String line = peekNextLine(reader, PemObject.RFC2440_MAX_LINE_LENGTH); + while (line != null && !(line.startsWith(PemObject.RFC1421_ENCAPSULATION_BEGIN_MARKER) || + line.startsWith(PemObject.RFC4716_ENCAPSULATION_BEGIN_MARKER))) { + line = reader.readLine(); + //Read "explanatory text" as defined by RFC 7468 + if (line.length() > 0) { + explanatoryTextBuilder.append(line).append("\n"); + } + line = peekNextLine(reader, PemObject.RFC2440_MAX_LINE_LENGTH); + } + return explanatoryTextBuilder.toString(); + } + + + /** + * Reads the next line in a {@link BufferedReader} instance without consuming it from the buffer + * + * @param reader {@link BufferedReader} instance + * @param maximumReadLength Maximum number of characters to peek + * @return Next line + * @throws IOException In case of errors reading the buffer + */ + private static String peekNextLine(final BufferedReader reader, final int maximumReadLength) throws IOException + { + reader.mark(maximumReadLength); + final String nextLine = reader.readLine(); + reader.reset(); + return nextLine; + } + + } + + + /** + * Descriptor for {@link org.bouncycastle.util.io.pem.PemObject} + * + * @author Middleware Services + */ + public static final class Descriptor + { + + /** + * The RFC PEM specification which governs the format of this PEM object. + */ + private final Format format; + + /** + * The type of this PEM encoded data (i.e. PUBLIC KEY). + */ + private final String type; + + /** + * Explanatory text prior to the encapsulation header as defined by RFC 7468 Section 5.2 + */ + private String explanatoryText; + + /** + * Constructor with RFC format and explanatory text. + * + * @param formatParam RFC governing PEM format + * @param explanatoryTextParam Explanatory text if applicable + * @param typeParam Data type + */ + public Descriptor(final Format formatParam, final String explanatoryTextParam, final String typeParam) + { + this.format = formatParam; + this.type = typeParam; + if (this.format == Format.RFC7468) { + this.explanatoryText = explanatoryTextParam; + } + } + + /** + * @return The RFC {@link Format} governing this PEM data. + */ + public Format getFormat() + { + return format; + } + + /** + * @return The encoded data type + */ + public String getType() + { + return type; + } + + /** + * @return The explanatory text included with this PEM as defined by RFC-7468 5.2 null is returned if this PEM file + * is not of {@link Format#RFC7468} + */ + public String getExplanatoryText() + { + return explanatoryText; + } + } +} diff --git a/src/main/java/org/cryptacular/ssh/SSHPublicKeyDecoder.java b/src/main/java/org/cryptacular/ssh/SSHPublicKeyDecoder.java new file mode 100644 index 00000000..bf83d08f --- /dev/null +++ b/src/main/java/org/cryptacular/ssh/SSHPublicKeyDecoder.java @@ -0,0 +1,134 @@ +/* See LICENSE for licensing and NOTICE for copyright. */ +package org.cryptacular.ssh; + +import java.math.BigInteger; +import org.bouncycastle.crypto.params.AsymmetricKeyParameter; +import org.bouncycastle.crypto.params.DSAParameters; +import org.bouncycastle.crypto.params.DSAPublicKeyParameters; +import org.bouncycastle.crypto.params.RSAKeyParameters; +import org.cryptacular.EncodingException; +import org.cryptacular.KeyDecoder; +import org.cryptacular.util.ByteUtil; +import org.cryptacular.util.CodecUtil; + +/** + * Decodes public keys formatted as described in RFC 4253 Section 6.6. Public Key Algorithms. Currently RSA and DSS key + * formats are supported. + * + * @author Middleware Services + */ +public class SSHPublicKeyDecoder implements KeyDecoder +{ + + /** + * Represents the current position of the buffer. This value is set to 0 every time decode is called. + */ + private int position; + + /** + * Makes a guess as to whether or not the encoded bytes contain an SSH public key. (Does it start with "ssh-" ?) + * + * @param encodedString encoded + * @return true if encoding format is probable, false otherwise + */ + public static boolean isRFC4253EncodedPublicKey(final String encodedString) + { + return encodedString.startsWith("ssh-"); + } + + + /** + * Makes a guess as to whether or not the encoded bytes contain an SSH public key. (Does it start with "ssh-" ?) + * + * @param bytes encoded bytes + * @return true if encoding format is probable, false otherwise + */ + public static boolean isRFC4253EncodedPublicKey(final byte[] bytes) + { + return isRFC4253EncodedPublicKey(new String(bytes, 0, 10, ByteUtil.ASCII_CHARSET).trim()); + } + + @Override + public AsymmetricKeyParameter decode(final byte[] bytes, final Object... args) throws EncodingException + { + position = 0; + final String type = decodeString(bytes); + switch (type) { + case "ssh-rsa": + final BigInteger e = decodeMPInt(bytes); + final BigInteger m = decodeMPInt(bytes); + return new RSAKeyParameters(false, m, e); + case "ssh-dss": + final BigInteger p = decodeMPInt(bytes); + final BigInteger q = decodeMPInt(bytes); + final BigInteger g = decodeMPInt(bytes); + final BigInteger y = decodeMPInt(bytes); + return new DSAPublicKeyParameters(y, new DSAParameters(p, q, g)); + default: + throw new EncodingException("Unsupported SSH2 public key type: " + type); + } + } + + public AsymmetricKeyParameter decode(final String pubData) + { + final String[] tokenized = pubData.trim().split("\\s+"); + if (tokenized.length < 2) { + throw new EncodingException("Unsupported SSH public key type"); + } + return decode(CodecUtil.b64(tokenized[1])); + } + + + /** + * Decodes a Java String from a string as per RFC 4251 Section 5 using the current position of the bytes buffer + * + * @param bytes buffered bytes array to read from + * @return {@link String} representing string + */ + private String decodeString(final byte[] bytes) + { + int length = decodeUInt32(bytes); + if (length < 0 || length > 256 * 1024) { + length = 256 * 1024; + } + final String type = new String(bytes, position, length); + position += length; + return type; + } + + + /** + * Decodes a Java int from a uint32 as per RFC 4251 Section 5 using the current position of the bytes buffer + * + * @param bytes buffered bytes array to read from + * @return {@link BigInteger} representing mpint + */ + private int decodeUInt32(final byte[] bytes) + { + final byte[] relevantBytes = new byte[4]; + System.arraycopy(bytes, position, relevantBytes, 0, 4); + final int decoded = ByteUtil.toInt(relevantBytes); + position += 4; + return decoded; + } + + + /** + * Decodes a Java {@link BigInteger} from mpint as per RFC 4253 Section 6.6 using the current position of the bytes + * buffer + * + * @param bytes buffered bytes array to read from + * @return {@link BigInteger} representing mpint + */ + private BigInteger decodeMPInt(final byte[] bytes) + { + int length = decodeUInt32(bytes); + if (length < 0 || length > 8 * 1024) { + length = 8 * 1024; + } + final byte[] bigIntBytes = new byte[length]; + System.arraycopy(bytes, position, bigIntBytes, 0, length); + position += length; + return new BigInteger(bigIntBytes); + } +} diff --git a/src/main/java/org/cryptacular/util/PemUtil.java b/src/main/java/org/cryptacular/util/PemUtil.java index 7f4ea60f..56d7dc63 100644 --- a/src/main/java/org/cryptacular/util/PemUtil.java +++ b/src/main/java/org/cryptacular/util/PemUtil.java @@ -1,10 +1,13 @@ /* See LICENSE for licensing and NOTICE for copyright. */ package org.cryptacular.util; +import java.io.BufferedReader; +import java.io.IOException; import java.nio.ByteBuffer; import java.nio.CharBuffer; import java.util.regex.Pattern; import org.cryptacular.codec.Base64Decoder; +import org.cryptacular.io.pem.PemObject; /** * Utility class with helper methods for common PEM encoding operations. @@ -14,20 +17,45 @@ public final class PemUtil { - /** Line length. */ - public static final int LINE_LENGTH = 64; + /** + * Line length. + * + * @deprecated Use {@link PemObject#RFC1421_MAX_LINE_LENGTH}. + */ + @Deprecated + public static final int LINE_LENGTH = PemObject.RFC1421_MAX_LINE_LENGTH; - /** PEM encoding header start string. */ - public static final String HEADER_BEGIN = "-----BEGIN"; + /** + * PEM encoding header start string. + * + * @deprecated Use {@link PemObject#RFC1421_ENCAPSULATION_BEGIN_MARKER}. + */ + @Deprecated + public static final String HEADER_BEGIN = PemObject.RFC1421_ENCAPSULATION_BEGIN_MARKER; - /** PEM encoding footer start string. */ - public static final String FOOTER_END = "-----END"; + /** + * PEM encoding footer start string. + * + * @deprecated Use {@link PemObject#RFC1421_ENCAPSULATION_END_MARKER}. + */ + @Deprecated + public static final String FOOTER_END = PemObject.RFC1421_ENCAPSULATION_END_MARKER; - /** Procedure type tag for PEM-encoded private key in OpenSSL format. */ - public static final String PROC_TYPE = "Proc-Type:"; + /** + * Procedure type tag for PEM-encoded private key in OpenSSL format. + * + * @deprecated Use {@link PemObject#RFC1421_HEADER_FIELD_PROC_TYPE}. + */ + @Deprecated + public static final String PROC_TYPE = PemObject.RFC1421_HEADER_FIELD_PROC_TYPE; - /** Decryption infor tag for PEM-encoded private key in OpenSSL format. */ - public static final String DEK_INFO = "DEK-Info:"; + /** + * Decryption info tag for PEM-encoded private key in OpenSSL format. + * + * @deprecated Use {@link PemObject#RFC1421_HEADER_FIELD_DEK_INFO}. + */ + @Deprecated + public static final String DEK_INFO = PemObject.RFC1421_HEADER_FIELD_DEK_INFO; /** Pattern used to split multiple PEM-encoded objects in a single file. */ private static final Pattern PEM_SPLITTER = Pattern.compile("-----(?:BEGIN|END) [A-Z ]+-----"); @@ -52,13 +80,15 @@ private PemUtil() {} public static boolean isPem(final byte[] data) { final String start = new String(data, 0, 10, ByteUtil.ASCII_CHARSET).trim(); - if (!start.startsWith(HEADER_BEGIN) && !start.startsWith(PROC_TYPE)) { + if (!start.startsWith(PemObject.RFC1421_ENCAPSULATION_BEGIN_MARKER) && + !start.startsWith(PemObject.RFC4716_ENCAPSULATION_BEGIN_MARKER) && + !start.startsWith(PemObject.RFC1421_HEADER_FIELD_PROC_TYPE)) { // Check all bytes in first line to make sure they are in the range // of base64 character set encoding - for (int i = 0; i < LINE_LENGTH; i++) { + for (int i = 0; i < PemObject.RFC7468_MAX_LINE_LENGTH; i++) { if (!isBase64Char(data[i])) { // Last two bytes may be padding character '=' (61) - if (i > LINE_LENGTH - 3) { + if (i > PemObject.RFC7468_MAX_LINE_LENGTH - 3) { if (data[i] != 61) { return false; } @@ -119,7 +149,8 @@ public static byte[] decode(final String pem) for (String object : PEM_SPLITTER.split(pem)) { buffer.clear(); for (String line : LINE_SPLITTER.split(object)) { - if (line.startsWith(DEK_INFO) || line.startsWith(PROC_TYPE)) { + if (line.startsWith(PemObject.RFC1421_HEADER_FIELD_DEK_INFO) || + line.startsWith(PemObject.RFC1421_HEADER_FIELD_PROC_TYPE)) { continue; } buffer.append(line); @@ -131,4 +162,21 @@ public static byte[] decode(final String pem) output.flip(); return ByteUtil.toArray(output); } + + + /** + * Reads the contents of a {@link BufferedReader} pointing to PEM data. + * + * @param reader {@link BufferedReader} reader that contains the data to parse + * + * @return {@link PemObject} instance with the data read + * + * @throws IOException In case of exceptions reading the buffer + * @throws IllegalArgumentException In case of malformed PEM data + */ + public static PemObject read(final BufferedReader reader) + throws IOException, IllegalArgumentException + { + return new PemObject.Builder().build(reader); + } } diff --git a/src/test/java/org/cryptacular/util/KeyPairUtilTest.java b/src/test/java/org/cryptacular/util/KeyPairUtilTest.java index 27349810..7658bcb1 100644 --- a/src/test/java/org/cryptacular/util/KeyPairUtilTest.java +++ b/src/test/java/org/cryptacular/util/KeyPairUtilTest.java @@ -317,6 +317,10 @@ public Object[][] getPublicKeyFiles() }, new Object[] {KEY_PATH + "rsa-pub.der", RSAPublicKey.class}, new Object[] {KEY_PATH + "rsa-pub.pem", RSAPublicKey.class}, + new Object[] {KEY_PATH + "ssh2-rsa-pub.pem", RSAPublicKey.class}, + new Object[] {KEY_PATH + "ssh2-dsa-pub.pem", DSAPublicKey.class}, + new Object[] {KEY_PATH + "ssh2-rsa-pub.pub", RSAPublicKey.class}, + new Object[] {KEY_PATH + "ssh2-dsa-pub.pub", DSAPublicKey.class}, }; } diff --git a/src/test/resources/keys/ssh2-dsa-pub.pem b/src/test/resources/keys/ssh2-dsa-pub.pem new file mode 100644 index 00000000..8d893506 --- /dev/null +++ b/src/test/resources/keys/ssh2-dsa-pub.pem @@ -0,0 +1,14 @@ +---- BEGIN SSH2 PUBLIC KEY ---- +Comment: 1024-bit DSA, converted by ememisya@tardis from OpenSSH +Subject: This is the same thing but it is not wrapped by any quotat\ +ions so it's a bit older in formatting yet perfectly valid. +AAAAB3NzaC1kc3MAAACBAK2L1LsVoAm3pu13mehY4DASyy6W9xziUnVB3S/MWtY8cdP5gW +Ez9bn+ax6sjiT8FBPGDXgy3BkhAvxT30jEQpcp0HtvNtg+E51ascV0bdiQToZMPodEzUrV +Cel3RvjN1X5iw291LaYwAFzD/hkvIfYYsHdwTfY12t4q22oXNb+dAAAAFQDAshpzRNhOHJ +Vxo72i6Af8Qokt3wAAAIEAhfGYLtGlC9FxXaVQe14vVGAJ+cceH8QbCEA2KqyJGbpGJ5hr +7ofwWOqIkBK9lZMGAiZcx9vnAfdVuWUMnIurmFuuFJ0M7Md+J3RgE0qglcU7mToJopBcSj +AsnwTbNxgciyGOkiGYKNm4j6qhMUv9ncNZ+1HJRt8fY5+DyoCasu8AAACAeaUB4Z3PbynG +Q6HgcGmiqFx9/zEOgzZIxPwasR56DIk8hZyvG4EBb0+02lg6UArGcfjKa5Ig8jI+Bjy3Iy +d89X+/OI8ypeqNHzWrnjJy+Ns8NapRIGczUN0W5P8VyU9QR5AMlUrD/f7rOZRFcpKunzOs +8TAIslOKHd5KWrQesKQ= +---- END SSH2 PUBLIC KEY ---- diff --git a/src/test/resources/keys/ssh2-dsa-pub.pub b/src/test/resources/keys/ssh2-dsa-pub.pub new file mode 100644 index 00000000..bf8dca28 --- /dev/null +++ b/src/test/resources/keys/ssh2-dsa-pub.pub @@ -0,0 +1 @@ +ssh-dss AAAAB3NzaC1kc3MAAACBAK2L1LsVoAm3pu13mehY4DASyy6W9xziUnVB3S/MWtY8cdP5gWEz9bn+ax6sjiT8FBPGDXgy3BkhAvxT30jEQpcp0HtvNtg+E51ascV0bdiQToZMPodEzUrVCel3RvjN1X5iw291LaYwAFzD/hkvIfYYsHdwTfY12t4q22oXNb+dAAAAFQDAshpzRNhOHJVxo72i6Af8Qokt3wAAAIEAhfGYLtGlC9FxXaVQe14vVGAJ+cceH8QbCEA2KqyJGbpGJ5hr7ofwWOqIkBK9lZMGAiZcx9vnAfdVuWUMnIurmFuuFJ0M7Md+J3RgE0qglcU7mToJopBcSjAsnwTbNxgciyGOkiGYKNm4j6qhMUv9ncNZ+1HJRt8fY5+DyoCasu8AAACAeaUB4Z3PbynGQ6HgcGmiqFx9/zEOgzZIxPwasR56DIk8hZyvG4EBb0+02lg6UArGcfjKa5Ig8jI+Bjy3Iyd89X+/OI8ypeqNHzWrnjJy+Ns8NapRIGczUN0W5P8VyU9QR5AMlUrD/f7rOZRFcpKunzOs8TAIslOKHd5KWrQesKQ= ememisya@tardis diff --git a/src/test/resources/keys/ssh2-rsa-pub.pem b/src/test/resources/keys/ssh2-rsa-pub.pem new file mode 100644 index 00000000..7845d6a2 --- /dev/null +++ b/src/test/resources/keys/ssh2-rsa-pub.pem @@ -0,0 +1,12 @@ +---- BEGIN SSH2 PUBLIC KEY ---- +Comment: "2048-bit RSA, converted by ememisya@tardis from OpenSSH" +Subject: "This is a long comment line which will be broken up with \ +a backslash indicating that the line wraps onto the next line in \ +the buffer" +AAAAB3NzaC1yc2EAAAADAQABAAABAQCyGi7s+xvDnPjizcLhCK+ItFjppDdpc/A8m0jwtX +x5JLbvhiWJ1WVM0OMxvzlD1UlPaFFYc716za30SpdO5lglB0WfxtlLdg+gBhwaTQ8Wy5x2 +/crYPbq75vRlsWg7qUJUmMt/aH7ii8zRaDOEhqY6nclA+2B2BXwLpiScE8WgKFTr+Gp6M+ +ka4MLX1dUpeV24Nb0Wr0naj8pgdhvhdGX2K13W43dSpDSnqGtL+JDvkfBKoqB031qXBABn +IyHaUYNmfTY7+Z3VvDO8zNMzA20Tgz2XIDx4sFW/3b8g6dYJD+f3T42ULY6e5U+85a0R6f +CTwJxcDeK1X582ZDPAKZa3 +---- END SSH2 PUBLIC KEY ---- diff --git a/src/test/resources/keys/ssh2-rsa-pub.pub b/src/test/resources/keys/ssh2-rsa-pub.pub new file mode 100644 index 00000000..a5d20949 --- /dev/null +++ b/src/test/resources/keys/ssh2-rsa-pub.pub @@ -0,0 +1 @@ +ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCyGi7s+xvDnPjizcLhCK+ItFjppDdpc/A8m0jwtXx5JLbvhiWJ1WVM0OMxvzlD1UlPaFFYc716za30SpdO5lglB0WfxtlLdg+gBhwaTQ8Wy5x2/crYPbq75vRlsWg7qUJUmMt/aH7ii8zRaDOEhqY6nclA+2B2BXwLpiScE8WgKFTr+Gp6M+ka4MLX1dUpeV24Nb0Wr0naj8pgdhvhdGX2K13W43dSpDSnqGtL+JDvkfBKoqB031qXBABnIyHaUYNmfTY7+Z3VvDO8zNMzA20Tgz2XIDx4sFW/3b8g6dYJD+f3T42ULY6e5U+85a0R6fCTwJxcDeK1X582ZDPAKZa3 ememisya@tardis