http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0d7af8c8/sshd-core/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java new file mode 100644 index 0000000..d180e9e --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java @@ -0,0 +1,382 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.common.config.keys.impl; + +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.io.StreamCorruptedException; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchProviderException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.util.Collections; +import java.util.EnumSet; +import java.util.Objects; +import java.util.Set; + +import org.apache.sshd.common.cipher.ECCurves; +import org.apache.sshd.common.config.keys.KeyEntryResolver; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.util.NumberUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class ECDSAPublicKeyEntryDecoder extends AbstractPublicKeyEntryDecoder<ECPublicKey, ECPrivateKey> { + public static final ECDSAPublicKeyEntryDecoder INSTANCE = new ECDSAPublicKeyEntryDecoder(); + + // see rfc5480 section 2.2 + public static final byte ECPOINT_UNCOMPRESSED_FORM_INDICATOR = 0x04; + public static final byte ECPOINT_COMPRESSED_VARIANT_2 = 0x02; + public static final byte ECPOINT_COMPRESSED_VARIANT_3 = 0x02; + + public ECDSAPublicKeyEntryDecoder() { + super(ECPublicKey.class, ECPrivateKey.class, ECCurves.KEY_TYPES); + } + + @Override + public ECPublicKey decodePublicKey(String keyType, InputStream keyData) throws IOException, GeneralSecurityException { + ECCurves curve = ECCurves.fromKeyType(keyType); + if (curve == null) { + throw new InvalidKeySpecException("Not an EC curve name: " + keyType); + } + + if (!SecurityUtils.hasEcc()) { + throw new NoSuchProviderException("ECC not supported"); + } + + String keyCurveName = curve.getName(); + ECParameterSpec paramSpec = curve.getParameters(); + // see rfc5656 section 3.1 + String encCurveName = KeyEntryResolver.decodeString(keyData); + if (!keyCurveName.equals(encCurveName)) { + throw new InvalidKeySpecException("Mismatched key curve name (" + keyCurveName + ") vs. encoded one (" + encCurveName + ")"); + } + + byte[] octets = KeyEntryResolver.readRLEBytes(keyData); + ECPoint w; + try { + w = octetStringToEcPoint(octets); + if (w == null) { + throw new InvalidKeySpecException("No ECPoint generated for curve=" + keyCurveName + + " from octets=" + BufferUtils.toHex(':', octets)); + } + } catch (RuntimeException e) { + throw new InvalidKeySpecException("Failed (" + e.getClass().getSimpleName() + ")" + + " to generate ECPoint for curve=" + keyCurveName + + " from octets=" + BufferUtils.toHex(':', octets) + + ": " + e.getMessage()); + } + + return generatePublicKey(new ECPublicKeySpec(w, paramSpec)); + } + + @Override + public ECPublicKey clonePublicKey(ECPublicKey key) throws GeneralSecurityException { + if (!SecurityUtils.hasEcc()) { + throw new NoSuchProviderException("ECC not supported"); + } + + if (key == null) { + return null; + } + + ECParameterSpec params = key.getParams(); + if (params == null) { + throw new InvalidKeyException("Missing parameters in key"); + } + + return generatePublicKey(new ECPublicKeySpec(key.getW(), params)); + } + + @Override + public ECPrivateKey clonePrivateKey(ECPrivateKey key) throws GeneralSecurityException { + if (!SecurityUtils.hasEcc()) { + throw new NoSuchProviderException("ECC not supported"); + } + + if (key == null) { + return null; + } + + ECParameterSpec params = key.getParams(); + if (params == null) { + throw new InvalidKeyException("Missing parameters in key"); + } + + return generatePrivateKey(new ECPrivateKeySpec(key.getS(), params)); + } + + @Override + public String encodePublicKey(OutputStream s, ECPublicKey key) throws IOException { + Objects.requireNonNull(key, "No public key provided"); + + ECParameterSpec params = Objects.requireNonNull(key.getParams(), "No EC parameters available"); + ECCurves curve = Objects.requireNonNull(ECCurves.fromCurveParameters(params), "Cannot determine curve"); + String keyType = curve.getKeyType(); + String curveName = curve.getName(); + KeyEntryResolver.encodeString(s, keyType); + // see rfc5656 section 3.1 + KeyEntryResolver.encodeString(s, curveName); + ECPointCompression.UNCOMPRESSED.writeECPoint(s, curveName, key.getW()); + return keyType; + } + + @Override + public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException { + if (SecurityUtils.hasEcc()) { + return SecurityUtils.getKeyFactory(KeyUtils.EC_ALGORITHM); + } else { + throw new NoSuchProviderException("ECC not supported"); + } + } + + @Override + public KeyPair generateKeyPair(int keySize) throws GeneralSecurityException { + ECCurves curve = ECCurves.fromCurveSize(keySize); + if (curve == null) { + throw new InvalidKeySpecException("Unknown curve for key size=" + keySize); + } + + KeyPairGenerator gen = getKeyPairGenerator(); + gen.initialize(curve.getParameters()); + return gen.generateKeyPair(); + } + + @Override + public KeyPairGenerator getKeyPairGenerator() throws GeneralSecurityException { + if (SecurityUtils.hasEcc()) { + return SecurityUtils.getKeyPairGenerator(KeyUtils.EC_ALGORITHM); + } else { + throw new NoSuchProviderException("ECC not supported"); + } + } + + public static ECPoint octetStringToEcPoint(byte... octets) { + if (NumberUtils.isEmpty(octets)) { + return null; + } + + int startIndex = findFirstNonZeroIndex(octets); + if (startIndex < 0) { + throw new IllegalArgumentException("All zeroes ECPoint N/A"); + } + + byte indicator = octets[startIndex]; + ECPointCompression compression = ECPointCompression.fromIndicatorValue(indicator); + if (compression == null) { + throw new UnsupportedOperationException("Unknown compression indicator value: 0x" + Integer.toHexString(indicator & 0xFF)); + } + + // The coordinates actually start after the compression indicator + return compression.octetStringToEcPoint(octets, startIndex + 1, octets.length - startIndex - 1); + } + + private static int findFirstNonZeroIndex(byte... octets) { + if (NumberUtils.isEmpty(octets)) { + return -1; + } + + for (int index = 0; index < octets.length; index++) { + if (octets[index] != 0) { + return index; + } + } + + return -1; // all zeroes + } + + /** + * The various {@link ECPoint} representation compression indicators + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + * @see <A HREF="https://www.ietf.org/rfc/rfc5480.txt">RFC-5480 - section 2.2</A> + */ + public enum ECPointCompression { + // see http://tools.ietf.org/html/draft-jivsov-ecc-compact-00 + // see http://crypto.stackexchange.com/questions/8914/ecdsa-compressed-public-key-point-back-to-uncompressed-public-key-point + VARIANT2((byte) 0x02) { + @Override + public ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len) { + byte[] xp = new byte[len]; + System.arraycopy(octets, startIndex, xp, 0, len); + BigInteger x = octetStringToInteger(xp); + + // TODO derive even Y... + throw new UnsupportedOperationException("octetStringToEcPoint(" + name() + ")(X=" + x + ") compression support N/A"); + } + }, + VARIANT3((byte) 0x03) { + @Override + public ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len) { + byte[] xp = new byte[len]; + System.arraycopy(octets, startIndex, xp, 0, len); + BigInteger x = octetStringToInteger(xp); + + // TODO derive odd Y... + throw new UnsupportedOperationException("octetStringToEcPoint(" + name() + ")(X=" + x + ") compression support N/A"); + } + }, + UNCOMPRESSED((byte) 0x04) { + @Override + public ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len) { + int numElements = len / 2; /* x, y */ + if (len != (numElements * 2)) { // make sure length is not odd + throw new IllegalArgumentException("octetStringToEcPoint(" + name() + ") " + + " invalid remainder octets representation: " + + " expected=" + (2 * numElements) + ", actual=" + len); + } + + byte[] xp = new byte[numElements]; + byte[] yp = new byte[numElements]; + System.arraycopy(octets, startIndex, xp, 0, numElements); + System.arraycopy(octets, startIndex + numElements, yp, 0, numElements); + + BigInteger x = octetStringToInteger(xp); + BigInteger y = octetStringToInteger(yp); + return new ECPoint(x, y); + } + + @Override + public void writeECPoint(OutputStream s, String curveName, ECPoint p) throws IOException { + ECCurves curve = ECCurves.fromCurveName(curveName); + if (curve == null) { + throw new StreamCorruptedException("writeECPoint(" + name() + ")[" + curveName + "] cannot determine octets count"); + } + + int numElements = curve.getNumPointOctets(); + KeyEntryResolver.encodeInt(s, 1 /* the indicator */ + 2 * numElements); + s.write(getIndicatorValue()); + writeCoordinate(s, "X", p.getAffineX(), numElements); + writeCoordinate(s, "Y", p.getAffineY(), numElements); + } + + }; + + public static final Set<ECPointCompression> VALUES = + Collections.unmodifiableSet(EnumSet.allOf(ECPointCompression.class)); + + private final byte indicatorValue; + + ECPointCompression(byte indicator) { + indicatorValue = indicator; + } + + public final byte getIndicatorValue() { + return indicatorValue; + } + + public abstract ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len); + + public byte[] ecPointToOctetString(String curveName, ECPoint p) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream((2 * 66) + Long.SIZE)) { + writeECPoint(baos, curveName, p); + return baos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("ecPointToOctetString(" + curveName + ")" + + " failed (" + e.getClass().getSimpleName() + ")" + + " to write data: " + e.getMessage(), + e); + } + } + + public void writeECPoint(OutputStream s, String curveName, ECPoint p) throws IOException { + if (s == null) { + throw new EOFException("No output stream"); + } + + throw new StreamCorruptedException("writeECPoint(" + name() + ")[" + p + "] N/A"); + } + + protected void writeCoordinate(OutputStream s, String n, BigInteger v, int numElements) throws IOException { + byte[] vp = v.toByteArray(); + int startIndex = 0; + int vLen = vp.length; + if (vLen > numElements) { + if (vp[0] == 0) { // skip artificial positive sign + startIndex++; + vLen--; + } + } + + if (vLen > numElements) { + throw new StreamCorruptedException("writeCoordinate(" + name() + ")[" + n + "]" + + " value length (" + vLen + ") exceeds max. (" + numElements + ")" + + " for " + v); + } + + if (vLen < numElements) { + byte[] tmp = new byte[numElements]; + System.arraycopy(vp, startIndex, tmp, numElements - vLen, vLen); + vp = tmp; + } + + s.write(vp, startIndex, vLen); + } + + public static ECPointCompression fromIndicatorValue(int value) { + if ((value < 0) || (value > 0xFF)) { + return null; // must be a byte value + } + + for (ECPointCompression c : VALUES) { + if (value == c.getIndicatorValue()) { + return c; + } + } + + return null; + } + + /** + * Converts the given octet string (defined by ASN.1 specifications) to a {@link BigInteger} + * As octet strings always represent positive integers, a zero-byte is prepended to + * the given array if necessary (if is MSB equal to 1), then this is converted to BigInteger + * The conversion is defined in the Section 2.3.8 + * + * @param octets - octet string bytes to be converted + * @return The {@link BigInteger} representation of the octet string + */ + public static BigInteger octetStringToInteger(byte... octets) { + if (octets == null) { + return null; + } else if (octets.length == 0) { + return BigInteger.ZERO; + } else { + return new BigInteger(1, octets); + } + } + } +}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0d7af8c8/sshd-core/src/main/java/org/apache/sshd/common/config/keys/impl/RSAPublicKeyDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/impl/RSAPublicKeyDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/impl/RSAPublicKeyDecoder.java new file mode 100644 index 0000000..4550815 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/impl/RSAPublicKeyDecoder.java @@ -0,0 +1,117 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.common.config.keys.impl; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.Collections; +import java.util.Objects; + +import org.apache.sshd.common.config.keys.KeyEntryResolver; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class RSAPublicKeyDecoder extends AbstractPublicKeyEntryDecoder<RSAPublicKey, RSAPrivateKey> { + public static final RSAPublicKeyDecoder INSTANCE = new RSAPublicKeyDecoder(); + + public RSAPublicKeyDecoder() { + super(RSAPublicKey.class, RSAPrivateKey.class, Collections.unmodifiableList(Collections.singletonList(KeyPairProvider.SSH_RSA))); + } + + @Override + public RSAPublicKey decodePublicKey(String keyType, InputStream keyData) throws IOException, GeneralSecurityException { + if (!KeyPairProvider.SSH_RSA.equals(keyType)) { // just in case we were invoked directly + throw new InvalidKeySpecException("Unexpected key type: " + keyType); + } + + BigInteger e = KeyEntryResolver.decodeBigInt(keyData); + BigInteger n = KeyEntryResolver.decodeBigInt(keyData); + + return generatePublicKey(new RSAPublicKeySpec(n, e)); + } + + @Override + public String encodePublicKey(OutputStream s, RSAPublicKey key) throws IOException { + Objects.requireNonNull(key, "No public key provided"); + KeyEntryResolver.encodeString(s, KeyPairProvider.SSH_RSA); + KeyEntryResolver.encodeBigInt(s, key.getPublicExponent()); + KeyEntryResolver.encodeBigInt(s, key.getModulus()); + + return KeyPairProvider.SSH_RSA; + } + + @Override + public RSAPublicKey clonePublicKey(RSAPublicKey key) throws GeneralSecurityException { + if (key == null) { + return null; + } else { + return generatePublicKey(new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent())); + } + } + + @Override + public RSAPrivateKey clonePrivateKey(RSAPrivateKey key) throws GeneralSecurityException { + if (key == null) { + return null; + } + + if (!(key instanceof RSAPrivateCrtKey)) { + throw new InvalidKeyException("Cannot clone a non-RSAPrivateCrtKey: " + key.getClass().getSimpleName()); + } + + RSAPrivateCrtKey rsaPrv = (RSAPrivateCrtKey) key; + return generatePrivateKey( + new RSAPrivateCrtKeySpec( + rsaPrv.getModulus(), + rsaPrv.getPublicExponent(), + rsaPrv.getPrivateExponent(), + rsaPrv.getPrimeP(), + rsaPrv.getPrimeQ(), + rsaPrv.getPrimeExponentP(), + rsaPrv.getPrimeExponentQ(), + rsaPrv.getCrtCoefficient())); + } + + @Override + public KeyPairGenerator getKeyPairGenerator() throws GeneralSecurityException { + return SecurityUtils.getKeyPairGenerator(KeyUtils.RSA_ALGORITHM); + } + + @Override + public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException { + return SecurityUtils.getKeyFactory(KeyUtils.RSA_ALGORITHM); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0d7af8c8/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractKeyPairResourceParser.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractKeyPairResourceParser.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractKeyPairResourceParser.java new file mode 100644 index 0000000..6f0d64a --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractKeyPairResourceParser.java @@ -0,0 +1,186 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.common.config.keys.loader; + +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.StreamCorruptedException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; +import java.util.logging.Level; + +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.util.Base64; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.Pair; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.apache.sshd.common.util.logging.AbstractLoggingBean; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public abstract class AbstractKeyPairResourceParser extends AbstractLoggingBean implements KeyPairResourceParser { + private final List<String> beginners; + private final List<String> enders; + private final List<List<String>> endingMarkers; + + /** + * @param beginners The markers that indicate the beginning of a parsing block + * @param enders The <U>matching</U> (by position) markers that indicate the end of a parsing block + */ + protected AbstractKeyPairResourceParser(List<String> beginners, List<String> enders) { + this.beginners = ValidateUtils.checkNotNullAndNotEmpty(beginners, "No begin markers"); + this.enders = ValidateUtils.checkNotNullAndNotEmpty(enders, "No end markers"); + ValidateUtils.checkTrue( + beginners.size() == enders.size(), "Mismatched begin(%d)/end(%d) markers sizes", beginners.size(), enders.size()); + endingMarkers = new ArrayList<>(enders.size()); + enders.forEach(m -> endingMarkers.add(Collections.singletonList(m))); + } + + public List<String> getBeginners() { + return beginners; + } + + public List<String> getEnders() { + return enders; + } + + /** + * @return A {@link List} of same size as the ending markers, where + * each ending marker is encapsulated inside a singleton list and + * resides as the <U>same index</U> as the marker it encapsulates + */ + public List<List<String>> getEndingMarkers() { + return endingMarkers; + } + + @Override + public boolean canExtractKeyPairs(String resourceKey, List<String> lines) throws IOException, GeneralSecurityException { + return KeyPairResourceParser.containsMarkerLine(lines, getBeginners()); + } + + @Override + public Collection<KeyPair> loadKeyPairs(String resourceKey, FilePasswordProvider passwordProvider, List<String> lines) + throws IOException, GeneralSecurityException { + Collection<KeyPair> keyPairs = Collections.emptyList(); + List<String> beginMarkers = getBeginners(); + List<List<String>> endMarkers = getEndingMarkers(); + for (Pair<Integer, Integer> markerPos = KeyPairResourceParser.findMarkerLine(lines, beginMarkers); markerPos != null;) { + int startIndex = markerPos.getKey(); + String startLine = lines.get(startIndex); + startIndex++; + + int markerIndex = markerPos.getValue(); + List<String> ender = endMarkers.get(markerIndex); + markerPos = KeyPairResourceParser.findMarkerLine(lines, startIndex, ender); + if (markerPos == null) { + throw new StreamCorruptedException("Missing end marker (" + ender + ") after line #" + startIndex); + } + + int endIndex = markerPos.getKey(); + String endLine = lines.get(endIndex); + Collection<KeyPair> kps = + extractKeyPairs(resourceKey, startLine, endLine, passwordProvider, lines.subList(startIndex, endIndex)); + if (GenericUtils.isNotEmpty(kps)) { + if (GenericUtils.isEmpty(keyPairs)) { + keyPairs = new LinkedList<>(kps); + } else { + keyPairs.addAll(kps); + } + } + + // see if there are more + markerPos = KeyPairResourceParser.findMarkerLine(lines, endIndex + 1, beginMarkers); + } + + return keyPairs; + } + + /** + * Extracts the key pairs within a <U>single</U> delimited by markers block of lines. By + * default cleans up the empty lines, joins them and converts them from BASE64 + * + * @param resourceKey A hint as to the origin of the text lines + * @param beginMarker The line containing the begin marker + * @param endMarker The line containing the end marker + * @param passwordProvider The {@link FilePasswordProvider} to use + * in case the data is encrypted - may be {@code null} if no encrypted + * @param lines The block of lines between the markers + * @return The extracted {@link KeyPair}s - may be {@code null}/empty if none. + * @throws IOException If failed to parse the data + * @throws GeneralSecurityException If failed to generate the keys + * @see #extractKeyPairs(String, String, String, FilePasswordProvider, byte[]) + */ + public Collection<KeyPair> extractKeyPairs( + String resourceKey, String beginMarker, String endMarker, FilePasswordProvider passwordProvider, List<String> lines) + throws IOException, GeneralSecurityException { + String data = GenericUtils.join(lines, ' '); + data = data.replaceAll("\\s", ""); + data = data.trim(); + + return extractKeyPairs(resourceKey, beginMarker, endMarker, passwordProvider, Base64.decodeString(data)); + } + + /** + * @param resourceKey A hint as to the origin of the text lines + * @param beginMarker The line containing the begin marker + * @param endMarker The line containing the end marker + * @param passwordProvider The {@link FilePasswordProvider} to use + * in case the data is encrypted - may be {@code null} if no encrypted + * @param bytes The decoded bytes from the lines containing the data + * @return The extracted {@link KeyPair}s - may be {@code null}/empty if none. + * @throws IOException If failed to parse the data + * @throws GeneralSecurityException If failed to generate the keys + * @see #extractKeyPairs(String, String, String, FilePasswordProvider, InputStream) + */ + public Collection<KeyPair> extractKeyPairs( + String resourceKey, String beginMarker, String endMarker, FilePasswordProvider passwordProvider, byte[] bytes) + throws IOException, GeneralSecurityException { + if (log.isTraceEnabled()) { + BufferUtils.dumpHex(getSimplifiedLogger(), Level.FINER, beginMarker, ':', 16, bytes); + } + + try (InputStream bais = new ByteArrayInputStream(bytes)) { + return extractKeyPairs(resourceKey, beginMarker, endMarker, passwordProvider, bais); + } + } + + /** + * @param resourceKey A hint as to the origin of the text lines + * @param beginMarker The line containing the begin marker + * @param endMarker The line containing the end marker + * @param passwordProvider The {@link FilePasswordProvider} to use + * in case the data is encrypted - may be {@code null} if no encrypted + * @param stream The decoded data {@link InputStream} + * @return The extracted {@link KeyPair}s - may be {@code null}/empty if none. + * @throws IOException If failed to parse the data + * @throws GeneralSecurityException If failed to generate the keys + */ + public abstract Collection<KeyPair> extractKeyPairs( + String resourceKey, String beginMarker, String endMarker, FilePasswordProvider passwordProvider, InputStream stream) + throws IOException, GeneralSecurityException; +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0d7af8c8/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceLoader.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceLoader.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceLoader.java new file mode 100644 index 0000000..fa6930a --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceLoader.java @@ -0,0 +1,129 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.common.config.keys.loader; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.Reader; +import java.io.StringReader; +import java.net.URL; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.util.io.IoUtils; + +/** + * Loads {@link KeyPair}s from text resources + * + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +@FunctionalInterface +public interface KeyPairResourceLoader { + /** + * An empty loader that never fails but always returns an empty list + */ + KeyPairResourceLoader EMPTY = (resourceKey, passwordProvider, lines) -> Collections.emptyList(); + + default Collection<KeyPair> loadKeyPairs(Path path, FilePasswordProvider passwordProvider, OpenOption... options) + throws IOException, GeneralSecurityException { + return loadKeyPairs(path, passwordProvider, StandardCharsets.UTF_8, options); + } + + default Collection<KeyPair> loadKeyPairs(Path path, FilePasswordProvider passwordProvider, Charset cs, OpenOption... options) + throws IOException, GeneralSecurityException { + try (InputStream stream = Files.newInputStream(path, options)) { + return loadKeyPairs(path.toString(), passwordProvider, stream, cs); + } + } + + default Collection<KeyPair> loadKeyPairs(URL url, FilePasswordProvider passwordProvider) + throws IOException, GeneralSecurityException { + return loadKeyPairs(url, passwordProvider, StandardCharsets.UTF_8); + } + + default Collection<KeyPair> loadKeyPairs(URL url, FilePasswordProvider passwordProvider, Charset cs) + throws IOException, GeneralSecurityException { + try (InputStream stream = Objects.requireNonNull(url, "No URL").openStream()) { + return loadKeyPairs(url.toExternalForm(), passwordProvider, stream, cs); + } + } + + default Collection<KeyPair> loadKeyPairs(String resourceKey, FilePasswordProvider passwordProvider, String data) + throws IOException, GeneralSecurityException { + try (Reader reader = new StringReader((data == null) ? "" : data)) { + return loadKeyPairs(resourceKey, passwordProvider, reader); + } + } + + default Collection<KeyPair> loadKeyPairs(String resourceKey, FilePasswordProvider passwordProvider, InputStream stream) + throws IOException, GeneralSecurityException { + return loadKeyPairs(resourceKey, passwordProvider, stream, StandardCharsets.UTF_8); + } + + default Collection<KeyPair> loadKeyPairs(String resourceKey, FilePasswordProvider passwordProvider, InputStream stream, Charset cs) + throws IOException, GeneralSecurityException { + try (Reader reader = new InputStreamReader( + Objects.requireNonNull(stream, "No stream instance"), Objects.requireNonNull(cs, "No charset"))) { + return loadKeyPairs(resourceKey, passwordProvider, reader); + } + } + + default Collection<KeyPair> loadKeyPairs(String resourceKey, FilePasswordProvider passwordProvider, Reader r) + throws IOException, GeneralSecurityException { + try (BufferedReader br = new BufferedReader(Objects.requireNonNull(r, "No reader instance"), IoUtils.DEFAULT_COPY_SIZE)) { + return loadKeyPairs(resourceKey, passwordProvider, br); + } + } + + default Collection<KeyPair> loadKeyPairs(String resourceKey, FilePasswordProvider passwordProvider, BufferedReader r) + throws IOException, GeneralSecurityException { + return loadKeyPairs(resourceKey, passwordProvider, IoUtils.readAllLines(r)); + } + + /** + * Loads key pairs from the given resource text lines + * + * @param resourceKey A hint as to the origin of the text lines + * @param passwordProvider The {@link FilePasswordProvider} to use + * in case the data is encrypted - may be {@code null} if no encrypted + * data is expected + * @param lines The {@link List} of lines as read from the resource + * @return The extracted {@link KeyPair}s - may be {@code null}/empty if none. + * <B>Note:</B> the resource loader may decide to skip unknown lines if + * more than one key pair type is encoded in it + * @throws IOException If failed to process the lines + * @throws GeneralSecurityException If failed to generate the keys from the + * parsed data + */ + Collection<KeyPair> loadKeyPairs(String resourceKey, FilePasswordProvider passwordProvider, List<String> lines) + throws IOException, GeneralSecurityException; +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0d7af8c8/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceParser.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceParser.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceParser.java new file mode 100644 index 0000000..beb592d --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceParser.java @@ -0,0 +1,168 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.common.config.keys.loader; + +import java.io.IOException; +import java.security.GeneralSecurityException; +import java.security.KeyPair; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.LinkedList; +import java.util.List; + +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.Pair; +import org.apache.sshd.common.util.ValidateUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public interface KeyPairResourceParser extends KeyPairResourceLoader { + /** + * An empty parser that never fails, but always report that it cannot + * extract key pairs and returns empty list if asked to load + */ + KeyPairResourceParser EMPTY = new KeyPairResourceParser() { + @Override + public Collection<KeyPair> loadKeyPairs(String resourceKey, FilePasswordProvider passwordProvider, List<String> lines) + throws IOException, GeneralSecurityException { + return Collections.emptyList(); + } + + @Override + public boolean canExtractKeyPairs(String resourceKey, List<String> lines) throws IOException, GeneralSecurityException { + return false; + } + + @Override + public String toString() { + return "EMPTY"; + } + }; + + /** + * @param resourceKey A hint as to the origin of the text lines + * @param lines The resource lines + * @return {@code true} if the parser can extract some key pairs from the lines + * @throws IOException If failed to process the lines + * @throws GeneralSecurityException If failed to extract information regarding + * the possibility to extract the key pairs + */ + boolean canExtractKeyPairs(String resourceKey, List<String> lines) throws IOException, GeneralSecurityException; + + static boolean containsMarkerLine(List<String> lines, String marker) { + return containsMarkerLine(lines, Collections.singletonList(ValidateUtils.checkNotNullAndNotEmpty(marker, "No marker"))); + } + + static boolean containsMarkerLine(List<String> lines, List<String> markers) { + return findMarkerLine(lines, markers) != null; + } + + /** + * Attempts to locate a line that contains one of the markers + * + * @param lines The list of lines to scan - ignored if {@code null}/empty + * @param markers The markers to match - ignored if {@code null}/empty + * @return A {@link Pair} whose key is the <U>first</U> line index + * that matched and value the matched marker index - {@code null} if no match found + * @see #findMarkerLine(List, int, List) + */ + static Pair<Integer, Integer> findMarkerLine(List<String> lines, List<String> markers) { + return findMarkerLine(lines, 0, markers); + } + + /** + * Attempts to locate a line that contains one of the markers + * + * @param lines The list of lines to scan - ignored if {@code null}/empty + * @param startLine The scan start line index + * @param markers The markers to match - ignored if {@code null}/empty + * @return A {@link Pair} whose key is the <U>first</U> line index + * that matched and value the matched marker index - {@code null} if no match found + */ + static Pair<Integer, Integer> findMarkerLine(List<String> lines, int startLine, List<String> markers) { + if (GenericUtils.isEmpty(lines) || GenericUtils.isEmpty(markers)) { + return null; + } + + for (int lineIndex = startLine; lineIndex < lines.size(); lineIndex++) { + String l = lines.get(lineIndex); + for (int markerIndex = 0; markerIndex < markers.size(); markerIndex++) { + String m = markers.get(markerIndex); + if (l.contains(m)) { + return new Pair<>(lineIndex, markerIndex); + } + } + } + + return null; + } + + static KeyPairResourceParser aggregate(KeyPairResourceParser ... parsers) { + return aggregate(Arrays.asList(ValidateUtils.checkNotNullAndNotEmpty(parsers, "No parsers to aggregate"))); + } + + static KeyPairResourceParser aggregate(Collection<? extends KeyPairResourceParser> parsers) { + ValidateUtils.checkNotNullAndNotEmpty(parsers, "No parsers to aggregate"); + return new KeyPairResourceParser() { + @Override + public Collection<KeyPair> loadKeyPairs(String resourceKey, FilePasswordProvider passwordProvider, List<String> lines) + throws IOException, GeneralSecurityException { + Collection<KeyPair> keyPairs = Collections.emptyList(); + for (KeyPairResourceParser p : parsers) { + if (!p.canExtractKeyPairs(resourceKey, lines)) { + continue; + } + + Collection<KeyPair> kps = p.loadKeyPairs(resourceKey, passwordProvider, lines); + if (GenericUtils.isEmpty(kps)) { + continue; + } + + if (GenericUtils.isEmpty(keyPairs)) { + keyPairs = new LinkedList<>(kps); + } else { + keyPairs.addAll(kps); + } + } + + return keyPairs; + } + + @Override + public boolean canExtractKeyPairs(String resourceKey, List<String> lines) throws IOException, GeneralSecurityException { + for (KeyPairResourceParser p : parsers) { + if (p.canExtractKeyPairs(resourceKey, lines)) { + return true; + } + } + + return false; + } + + @Override + public String toString() { + return KeyPairResourceParser.class.getSimpleName() + "[aggregate]"; + } + }; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0d7af8c8/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHDSSPrivateKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHDSSPrivateKeyEntryDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHDSSPrivateKeyEntryDecoder.java new file mode 100644 index 0000000..02cae65 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHDSSPrivateKeyEntryDecoder.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.common.config.keys.loader.openssh; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.interfaces.DSAParams; +import java.security.interfaces.DSAPrivateKey; +import java.security.interfaces.DSAPublicKey; +import java.security.spec.DSAPrivateKeySpec; +import java.security.spec.DSAPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.util.Collections; +import java.util.Objects; + +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyEntryResolver; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.impl.AbstractPrivateKeyEntryDecoder; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class OpenSSHDSSPrivateKeyEntryDecoder extends AbstractPrivateKeyEntryDecoder<DSAPublicKey, DSAPrivateKey> { + public static final OpenSSHDSSPrivateKeyEntryDecoder INSTANCE = new OpenSSHDSSPrivateKeyEntryDecoder(); + + public OpenSSHDSSPrivateKeyEntryDecoder() { + super(DSAPublicKey.class, DSAPrivateKey.class, Collections.unmodifiableList(Collections.singletonList(KeyPairProvider.SSH_DSS))); + } + + @Override + public DSAPrivateKey decodePrivateKey(String keyType, FilePasswordProvider passwordProvider, InputStream keyData) + throws IOException, GeneralSecurityException { + if (!KeyPairProvider.SSH_DSS.equals(keyType)) { // just in case we were invoked directly + throw new InvalidKeySpecException("Unexpected key type: " + keyType); + } + + BigInteger p = KeyEntryResolver.decodeBigInt(keyData); + BigInteger q = KeyEntryResolver.decodeBigInt(keyData); + BigInteger g = KeyEntryResolver.decodeBigInt(keyData); + BigInteger y = KeyEntryResolver.decodeBigInt(keyData); + Objects.requireNonNull(y, "No public key data"); // TODO run some validation on it + BigInteger x = KeyEntryResolver.decodeBigInt(keyData); + + return generatePrivateKey(new DSAPrivateKeySpec(x, p, q, g)); + } + + @Override + public String encodePrivateKey(OutputStream s, DSAPrivateKey key) throws IOException { + Objects.requireNonNull(key, "No private key provided"); + + DSAParams keyParams = Objects.requireNonNull(key.getParams(), "No DSA params available"); + BigInteger p = keyParams.getP(); + KeyEntryResolver.encodeBigInt(s, p); + KeyEntryResolver.encodeBigInt(s, keyParams.getQ()); + + BigInteger g = keyParams.getG(); + KeyEntryResolver.encodeBigInt(s, g); + + BigInteger x = key.getX(); + BigInteger y = g.modPow(x, p); + KeyEntryResolver.encodeBigInt(s, y); + KeyEntryResolver.encodeBigInt(s, x); + return KeyPairProvider.SSH_DSS; + } + + @Override + public boolean isPublicKeyRecoverySupported() { + return true; + } + + @Override + public DSAPublicKey recoverPublicKey(DSAPrivateKey privateKey) throws GeneralSecurityException { + // based on code from https://github.com/alexo/SAML-2.0/blob/master/java-opensaml/opensaml-security-api/src/main/java/org/opensaml/xml/security/SecurityHelper.java + DSAParams keyParams = privateKey.getParams(); + BigInteger p = keyParams.getP(); + BigInteger x = privateKey.getX(); + BigInteger q = keyParams.getQ(); + BigInteger g = keyParams.getG(); + BigInteger y = g.modPow(x, p); + return generatePublicKey(new DSAPublicKeySpec(y, p, q, g)); + } + + @Override + public DSAPublicKey clonePublicKey(DSAPublicKey key) throws GeneralSecurityException { + if (key == null) { + return null; + } + + DSAParams params = key.getParams(); + if (params == null) { + throw new InvalidKeyException("Missing parameters in key"); + } + + return generatePublicKey(new DSAPublicKeySpec(key.getY(), params.getP(), params.getQ(), params.getG())); + } + + @Override + public DSAPrivateKey clonePrivateKey(DSAPrivateKey key) throws GeneralSecurityException { + if (key == null) { + return null; + } + + DSAParams params = key.getParams(); + if (params == null) { + throw new InvalidKeyException("Missing parameters in key"); + } + + return generatePrivateKey(new DSAPrivateKeySpec(key.getX(), params.getP(), params.getQ(), params.getG())); + } + + @Override + public KeyPairGenerator getKeyPairGenerator() throws GeneralSecurityException { + return SecurityUtils.getKeyPairGenerator(KeyUtils.DSS_ALGORITHM); + } + + @Override + public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException { + return SecurityUtils.getKeyFactory(KeyUtils.DSS_ALGORITHM); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0d7af8c8/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHECDSAPrivateKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHECDSAPrivateKeyEntryDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHECDSAPrivateKeyEntryDecoder.java new file mode 100644 index 0000000..bdb6d28 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHECDSAPrivateKeyEntryDecoder.java @@ -0,0 +1,164 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.common.config.keys.loader.openssh; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPair; +import java.security.KeyPairGenerator; +import java.security.NoSuchProviderException; +import java.security.interfaces.ECPrivateKey; +import java.security.interfaces.ECPublicKey; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPrivateKeySpec; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.InvalidKeySpecException; +import java.util.Objects; + +import org.apache.sshd.common.cipher.ECCurves; +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyEntryResolver; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.impl.AbstractPrivateKeyEntryDecoder; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class OpenSSHECDSAPrivateKeyEntryDecoder extends AbstractPrivateKeyEntryDecoder<ECPublicKey, ECPrivateKey> { + public static final OpenSSHECDSAPrivateKeyEntryDecoder INSTANCE = new OpenSSHECDSAPrivateKeyEntryDecoder(); + + public OpenSSHECDSAPrivateKeyEntryDecoder() { + super(ECPublicKey.class, ECPrivateKey.class, ECCurves.KEY_TYPES); + } + + @Override + public ECPrivateKey decodePrivateKey(String keyType, FilePasswordProvider passwordProvider, InputStream keyData) + throws IOException, GeneralSecurityException { + ECCurves curve = ECCurves.fromKeyType(keyType); + if (curve == null) { + throw new InvalidKeySpecException("Not an EC curve name: " + keyType); + } + + if (!SecurityUtils.hasEcc()) { + throw new NoSuchProviderException("ECC not supported"); + } + + String keyCurveName = curve.getName(); + // see rfc5656 section 3.1 + String encCurveName = KeyEntryResolver.decodeString(keyData); + if (!keyCurveName.equals(encCurveName)) { + throw new InvalidKeySpecException("Mismatched key curve name (" + keyCurveName + ") vs. encoded one (" + encCurveName + ")"); + } + + byte[] pubKey = KeyEntryResolver.readRLEBytes(keyData); + Objects.requireNonNull(pubKey, "No public point"); // TODO validate it is a valid ECPoint + BigInteger s = KeyEntryResolver.decodeBigInt(keyData); + ECParameterSpec params = curve.getParameters(); + return generatePrivateKey(new ECPrivateKeySpec(s, params)); + } + + @Override + public String encodePrivateKey(OutputStream s, ECPrivateKey key) throws IOException { + Objects.requireNonNull(key, "No private key provided"); + return null; + } + + @Override + public ECPublicKey recoverPublicKey(ECPrivateKey prvKey) throws GeneralSecurityException { + ECCurves curve = ECCurves.fromECKey(prvKey); + if (curve == null) { + throw new InvalidKeyException("Unknown curve"); + } + // TODO see how we can figure out the public value + return super.recoverPublicKey(prvKey); + } + + @Override + public ECPublicKey clonePublicKey(ECPublicKey key) throws GeneralSecurityException { + if (!SecurityUtils.hasEcc()) { + throw new NoSuchProviderException("ECC not supported"); + } + + if (key == null) { + return null; + } + + ECParameterSpec params = key.getParams(); + if (params == null) { + throw new InvalidKeyException("Missing parameters in key"); + } + + return generatePublicKey(new ECPublicKeySpec(key.getW(), params)); + } + + @Override + public ECPrivateKey clonePrivateKey(ECPrivateKey key) throws GeneralSecurityException { + if (!SecurityUtils.hasEcc()) { + throw new NoSuchProviderException("ECC not supported"); + } + + if (key == null) { + return null; + } + + ECParameterSpec params = key.getParams(); + if (params == null) { + throw new InvalidKeyException("Missing parameters in key"); + } + + return generatePrivateKey(new ECPrivateKeySpec(key.getS(), params)); + } + + @Override + public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException { + if (SecurityUtils.hasEcc()) { + return SecurityUtils.getKeyFactory(KeyUtils.EC_ALGORITHM); + } else { + throw new NoSuchProviderException("ECC not supported"); + } + } + + @Override + public KeyPair generateKeyPair(int keySize) throws GeneralSecurityException { + ECCurves curve = ECCurves.fromCurveSize(keySize); + if (curve == null) { + throw new InvalidKeySpecException("Unknown curve for key size=" + keySize); + } + + KeyPairGenerator gen = getKeyPairGenerator(); + gen.initialize(curve.getParameters()); + return gen.generateKeyPair(); + } + + @Override + public KeyPairGenerator getKeyPairGenerator() throws GeneralSecurityException { + if (SecurityUtils.hasEcc()) { + return SecurityUtils.getKeyPairGenerator(KeyUtils.EC_ALGORITHM); + } else { + throw new NoSuchProviderException("ECC not supported"); + } + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0d7af8c8/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java new file mode 100644 index 0000000..bc41acb --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java @@ -0,0 +1,355 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.common.config.keys.loader.openssh; + +import java.io.ByteArrayInputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.InputStream; +import java.io.StreamCorruptedException; +import java.nio.charset.StandardCharsets; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.Key; +import java.security.KeyPair; +import java.security.NoSuchAlgorithmException; +import java.security.PrivateKey; +import java.security.PublicKey; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.TreeMap; + +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyEntryResolver; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.PrivateKeyEntryDecoder; +import org.apache.sshd.common.config.keys.PublicKeyEntryDecoder; +import org.apache.sshd.common.config.keys.loader.AbstractKeyPairResourceParser; +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.Pair; +import org.apache.sshd.common.util.ValidateUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; +import org.apache.sshd.common.util.io.IoUtils; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * Basic support for <A HREF="http://cvsweb.openbsd.org/cgi-bin/cvsweb/src/usr.bin/ssh/PROTOCOL.key?rev=1.1&content-type=text/x-cvsweb-markup">OpenSSH key file(s)</A> + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class OpenSSHKeyPairResourceParser extends AbstractKeyPairResourceParser { + public static final String BEGIN_MARKER = "BEGIN OPENSSH PRIVATE KEY"; + public static final List<String> BEGINNERS = + Collections.unmodifiableList(Collections.singletonList(BEGIN_MARKER)); + + public static final String END_MARKER = "END OPENSSH PRIVATE KEY"; + public static final List<String> ENDERS = + Collections.unmodifiableList(Collections.singletonList(END_MARKER)); + + public static final String AUTH_MAGIC = "openssh-key-v1"; + public static final OpenSSHKeyPairResourceParser INSTANCE = new OpenSSHKeyPairResourceParser(); + + private static final byte[] AUTH_MAGIC_BYTES = AUTH_MAGIC.getBytes(StandardCharsets.UTF_8); + private static final Map<String, PrivateKeyEntryDecoder<?, ?>> BY_KEY_TYPE_DECODERS_MAP = + new TreeMap<>(String.CASE_INSENSITIVE_ORDER); + + private static final Map<Class<?>, PrivateKeyEntryDecoder<?, ?>> BY_KEY_CLASS_DECODERS_MAP = + new HashMap<>(); + + static { + registerPrivateKeyEntryDecoder(OpenSSHRSAPrivateKeyDecoder.INSTANCE); + registerPrivateKeyEntryDecoder(OpenSSHDSSPrivateKeyEntryDecoder.INSTANCE); + + if (SecurityUtils.hasEcc()) { + registerPrivateKeyEntryDecoder(OpenSSHECDSAPrivateKeyEntryDecoder.INSTANCE); + } + if (SecurityUtils.isEDDSACurveSupported()) { + registerPrivateKeyEntryDecoder(SecurityUtils.getOpenSSHEDDSAPrivateKeyEntryDecoder()); + } + } + + public OpenSSHKeyPairResourceParser() { + super(BEGINNERS, ENDERS); + } + + @Override + public Collection<KeyPair> extractKeyPairs( + String resourceKey, String beginMarker, String endMarker, FilePasswordProvider passwordProvider, InputStream stream) + throws IOException, GeneralSecurityException { + stream = validateStreamMagicMarker(resourceKey, stream); + + String cipher = KeyEntryResolver.decodeString(stream); + if (!OpenSSHParserContext.IS_NONE_CIPHER.test(cipher)) { + throw new NoSuchAlgorithmException("Unsupported cipher: " + cipher); + } + if (log.isDebugEnabled()) { + log.debug("extractKeyPairs({}) cipher={}", resourceKey, cipher); + } + + String kdfName = KeyEntryResolver.decodeString(stream); + if (!OpenSSHParserContext.IS_NONE_KDF.test(kdfName)) { + throw new NoSuchAlgorithmException("Unsupported KDF: " + kdfName); + } + + byte[] kdfOptions = KeyEntryResolver.readRLEBytes(stream); + if (log.isDebugEnabled()) { + log.debug("extractKeyPairs({}) KDF={}, options={}", + resourceKey, kdfName, BufferUtils.toHex(':', kdfOptions)); + } + + int numKeys = KeyEntryResolver.decodeInt(stream); + if (numKeys <= 0) { + if (log.isDebugEnabled()) { + log.debug("extractKeyPairs({}) no encoded keys", resourceKey); + } + return Collections.emptyList(); + } + + List<PublicKey> publicKeys = new ArrayList<>(numKeys); + OpenSSHParserContext context = new OpenSSHParserContext(cipher, kdfName, kdfOptions); + boolean traceEnabled = log.isTraceEnabled(); + for (int index = 1; index <= numKeys; index++) { + PublicKey pubKey = readPublicKey(resourceKey, context, stream); + ValidateUtils.checkNotNull(pubKey, "Empty public key #%d in %s", index, resourceKey); + if (traceEnabled) { + log.trace("extractKeyPairs({}) read public key #{}: {} {}", + resourceKey, index, KeyUtils.getKeyType(pubKey), KeyUtils.getFingerPrint(pubKey)); + } + publicKeys.add(pubKey); + } + + byte[] privateData = KeyEntryResolver.readRLEBytes(stream); + try (InputStream bais = new ByteArrayInputStream(privateData)) { + return readPrivateKeys(resourceKey, context, publicKeys, passwordProvider, bais); + } + } + + protected PublicKey readPublicKey( + String resourceKey, OpenSSHParserContext context, InputStream stream) + throws IOException, GeneralSecurityException { + byte[] keyData = KeyEntryResolver.readRLEBytes(stream); + try (InputStream bais = new ByteArrayInputStream(keyData)) { + String keyType = KeyEntryResolver.decodeString(bais); + PublicKeyEntryDecoder<?, ?> decoder = KeyUtils.getPublicKeyEntryDecoder(keyType); + if (decoder == null) { + throw new NoSuchAlgorithmException("Unsupported key type (" + keyType + ") in " + resourceKey); + } + + return decoder.decodePublicKey(keyType, bais); + } + } + + protected List<KeyPair> readPrivateKeys( + String resourceKey, OpenSSHParserContext context, Collection<? extends PublicKey> publicKeys, + FilePasswordProvider passwordProvider, InputStream stream) + throws IOException, GeneralSecurityException { + if (GenericUtils.isEmpty(publicKeys)) { + return Collections.emptyList(); + } + + boolean traceEnabled = log.isTraceEnabled(); + int check1 = KeyEntryResolver.decodeInt(stream); + int check2 = KeyEntryResolver.decodeInt(stream); + if (traceEnabled) { + log.trace("readPrivateKeys({}) check1=0x{}, check2=0x{}", + resourceKey, Integer.toHexString(check1), Integer.toHexString(check2)); + } + + List<KeyPair> keyPairs = new ArrayList<>(publicKeys.size()); + for (PublicKey pubKey : publicKeys) { + String pubType = KeyUtils.getKeyType(pubKey); + int keyIndex = keyPairs.size() + 1; + if (traceEnabled) { + log.trace("extractKeyPairs({}) read private key #{}: {}", + resourceKey, keyIndex, pubType); + } + + Pair<PrivateKey, String> prvData = readPrivateKey(resourceKey, context, pubType, passwordProvider, stream); + PrivateKey prvKey = (prvData == null) ? null : prvData.getKey(); + ValidateUtils.checkNotNull(prvKey, "Empty private key #%d in %s", keyIndex, resourceKey); + + String prvType = KeyUtils.getKeyType(prvKey); + ValidateUtils.checkTrue(Objects.equals(pubType, prvType), + "Mismatched public (%s) vs. private (%s) key type #%d in %s", + pubType, prvType, keyIndex, resourceKey); + + if (traceEnabled) { + log.trace("extractKeyPairs({}) add private key #{}: {} {}", + resourceKey, keyIndex, prvType, prvData.getValue()); + } + keyPairs.add(new KeyPair(pubKey, prvKey)); + } + + return keyPairs; + } + + protected Pair<PrivateKey, String> readPrivateKey( + String resourceKey, OpenSSHParserContext context, String keyType, FilePasswordProvider passwordProvider, InputStream stream) + throws IOException, GeneralSecurityException { + String prvType = KeyEntryResolver.decodeString(stream); + if (!Objects.equals(keyType, prvType)) { + throw new StreamCorruptedException("Mismatched private key type: " + + ", expected=" + keyType + ", actual=" + prvType + + " in " + resourceKey); + } + + PrivateKeyEntryDecoder<?, ?> decoder = getPrivateKeyEntryDecoder(prvType); + if (decoder == null) { + throw new NoSuchAlgorithmException("Unsupported key type (" + prvType + ") in " + resourceKey); + } + + PrivateKey prvKey = decoder.decodePrivateKey(prvType, passwordProvider, stream); + if (prvKey == null) { + throw new InvalidKeyException("Cannot parse key type (" + prvType + ") in " + resourceKey); + } + + String comment = KeyEntryResolver.decodeString(stream); + return new Pair<>(prvKey, comment); + } + + protected <S extends InputStream> S validateStreamMagicMarker(String resourceKey, S stream) throws IOException { + byte[] actual = new byte[AUTH_MAGIC_BYTES.length]; + IoUtils.readFully(stream, actual); + if (!Arrays.equals(AUTH_MAGIC_BYTES, actual)) { + throw new StreamCorruptedException(resourceKey + ": Mismatched magic marker value: " + BufferUtils.toHex(':', actual)); + } + + int eos = stream.read(); + if (eos == -1) { + throw new EOFException(resourceKey + ": Premature EOF after magic marker value"); + } + + if (eos != 0) { + throw new StreamCorruptedException(resourceKey + ": Missing EOS after magic marker value: 0x" + Integer.toHexString(eos)); + } + + return stream; + } + /** + * @param decoder The decoder to register + * @throws IllegalArgumentException if no decoder or not key type or no + * supported names for the decoder + * @see PrivateKeyEntryDecoder#getPublicKeyType() + * @see PrivateKeyEntryDecoder#getSupportedTypeNames() + */ + public static void registerPrivateKeyEntryDecoder(PrivateKeyEntryDecoder<?, ?> decoder) { + Objects.requireNonNull(decoder, "No decoder specified"); + + Class<?> pubType = Objects.requireNonNull(decoder.getPublicKeyType(), "No public key type declared"); + Class<?> prvType = Objects.requireNonNull(decoder.getPrivateKeyType(), "No private key type declared"); + synchronized (BY_KEY_CLASS_DECODERS_MAP) { + BY_KEY_CLASS_DECODERS_MAP.put(pubType, decoder); + BY_KEY_CLASS_DECODERS_MAP.put(prvType, decoder); + } + + Collection<String> names = ValidateUtils.checkNotNullAndNotEmpty(decoder.getSupportedTypeNames(), "No supported key type"); + synchronized (BY_KEY_TYPE_DECODERS_MAP) { + for (String n : names) { + PrivateKeyEntryDecoder<?, ?> prev = BY_KEY_TYPE_DECODERS_MAP.put(n, decoder); + if (prev != null) { + //noinspection UnnecessaryContinue + continue; // debug breakpoint + } + } + } + } + + /** + * @param keyType The {@code OpenSSH} key type string - e.g., {@code ssh-rsa, ssh-dss} + * - ignored if {@code null}/empty + * @return The registered {@link PrivateKeyEntryDecoder} or {code null} if not found + */ + public static PrivateKeyEntryDecoder<?, ?> getPrivateKeyEntryDecoder(String keyType) { + if (GenericUtils.isEmpty(keyType)) { + return null; + } + + synchronized (BY_KEY_TYPE_DECODERS_MAP) { + return BY_KEY_TYPE_DECODERS_MAP.get(keyType); + } + } + + /** + * @param kp The {@link KeyPair} to examine - ignored if {@code null} + * @return The matching {@link PrivateKeyEntryDecoder} provided <U>both</U> + * the public and private keys have the same decoder - {@code null} if no + * match found + * @see #getPrivateKeyEntryDecoder(Key) + */ + public static PrivateKeyEntryDecoder<?, ?> getPrivateKeyEntryDecoder(KeyPair kp) { + if (kp == null) { + return null; + } + + PrivateKeyEntryDecoder<?, ?> d1 = getPrivateKeyEntryDecoder(kp.getPublic()); + PrivateKeyEntryDecoder<?, ?> d2 = getPrivateKeyEntryDecoder(kp.getPrivate()); + if (d1 == d2) { + return d1; + } else { + return null; // some kind of mixed keys... + } + } + + /** + * @param key The {@link Key} (public or private) - ignored if {@code null} + * @return The registered {@link PrivateKeyEntryDecoder} for this key or {code null} if no match found + * @see #getPrivateKeyEntryDecoder(Class) + */ + public static PrivateKeyEntryDecoder<?, ?> getPrivateKeyEntryDecoder(Key key) { + if (key == null) { + return null; + } else { + return getPrivateKeyEntryDecoder(key.getClass()); + } + } + + /** + * @param keyType The key {@link Class} - ignored if {@code null} or not a {@link Key} + * compatible type + * @return The registered {@link PrivateKeyEntryDecoder} or {code null} if no match found + */ + public static PrivateKeyEntryDecoder<?, ?> getPrivateKeyEntryDecoder(Class<?> keyType) { + if ((keyType == null) || (!Key.class.isAssignableFrom(keyType))) { + return null; + } + + synchronized (BY_KEY_TYPE_DECODERS_MAP) { + PrivateKeyEntryDecoder<?, ?> decoder = BY_KEY_CLASS_DECODERS_MAP.get(keyType); + if (decoder != null) { + return decoder; + } + + // in case it is a derived class + for (PrivateKeyEntryDecoder<?, ?> dec : BY_KEY_CLASS_DECODERS_MAP.values()) { + Class<?> pubType = dec.getPublicKeyType(); + Class<?> prvType = dec.getPrivateKeyType(); + if (pubType.isAssignableFrom(keyType) || prvType.isAssignableFrom(keyType)) { + return dec; + } + } + } + + return null; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0d7af8c8/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHParserContext.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHParserContext.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHParserContext.java new file mode 100644 index 0000000..07f2a9a --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHParserContext.java @@ -0,0 +1,83 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.common.config.keys.loader.openssh; + +import java.util.function.Predicate; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.buffer.BufferUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class OpenSSHParserContext { + public static final String NONE_CIPHER = "none"; + public static final Predicate<String> IS_NONE_CIPHER = c -> GenericUtils.isEmpty(c) || NONE_CIPHER.equalsIgnoreCase(c); + + public static final String NONE_KDF = "none"; + public static final Predicate<String> IS_NONE_KDF = c -> GenericUtils.isEmpty(c) || NONE_KDF.equalsIgnoreCase(c); + + private String cipherName; + private String kdfName; + private byte[] kdfOptions; + + public OpenSSHParserContext() { + super(); + } + + public OpenSSHParserContext(String cipherName, String kdfName, byte... kdfOptions) { + this.cipherName = cipherName; + this.kdfName = kdfName; + this.kdfOptions = kdfOptions; + } + + public String getCipherName() { + return cipherName; + } + + public void setCipherName(String cipherName) { + this.cipherName = cipherName; + } + + public String getKdfName() { + return kdfName; + } + + public void setKdfName(String kdfName) { + this.kdfName = kdfName; + } + + public byte[] getKdfOptions() { + return kdfOptions; + } + + public void setKdfOptions(byte[] kdfOptions) { + this.kdfOptions = kdfOptions; + } + + @Override + public String toString() { + return getClass().getSimpleName() + + "[cipher=" + getCipherName() + + ", kdfName=" + getKdfName() + + ", kdfOptions=" + BufferUtils.toHex(':', getKdfOptions()) + + "]"; + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0d7af8c8/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHRSAPrivateKeyDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHRSAPrivateKeyDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHRSAPrivateKeyDecoder.java new file mode 100644 index 0000000..e957d0b --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHRSAPrivateKeyDecoder.java @@ -0,0 +1,152 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.apache.sshd.common.config.keys.loader.openssh; + +import java.io.IOException; +import java.io.InputStream; +import java.math.BigInteger; +import java.security.GeneralSecurityException; +import java.security.InvalidKeyException; +import java.security.KeyFactory; +import java.security.KeyPairGenerator; +import java.security.interfaces.RSAPrivateCrtKey; +import java.security.interfaces.RSAPrivateKey; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPrivateCrtKeySpec; +import java.security.spec.RSAPrivateKeySpec; +import java.security.spec.RSAPublicKeySpec; +import java.util.Collections; +import java.util.Objects; + +import org.apache.sshd.common.config.keys.FilePasswordProvider; +import org.apache.sshd.common.config.keys.KeyEntryResolver; +import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.impl.AbstractPrivateKeyEntryDecoder; +import org.apache.sshd.common.keyprovider.KeyPairProvider; +import org.apache.sshd.common.util.security.SecurityUtils; + +/** + * @author <a href="mailto:[email protected]">Apache MINA SSHD Project</a> + */ +public class OpenSSHRSAPrivateKeyDecoder extends AbstractPrivateKeyEntryDecoder<RSAPublicKey, RSAPrivateKey> { + public static final BigInteger DEFAULT_PUBLIC_EXPONENT = new BigInteger("65537"); + public static final OpenSSHRSAPrivateKeyDecoder INSTANCE = new OpenSSHRSAPrivateKeyDecoder(); + + public OpenSSHRSAPrivateKeyDecoder() { + super(RSAPublicKey.class, RSAPrivateKey.class, Collections.unmodifiableList(Collections.singletonList(KeyPairProvider.SSH_RSA))); + } + + @Override + public RSAPrivateKey decodePrivateKey(String keyType, FilePasswordProvider passwordProvider, InputStream keyData) + throws IOException, GeneralSecurityException { + if (!KeyPairProvider.SSH_RSA.equals(keyType)) { // just in case we were invoked directly + throw new InvalidKeySpecException("Unexpected key type: " + keyType); + } + + BigInteger n = KeyEntryResolver.decodeBigInt(keyData); + BigInteger e = KeyEntryResolver.decodeBigInt(keyData); + if (!Objects.equals(e, DEFAULT_PUBLIC_EXPONENT)) { + log.warn("decodePrivateKey({}) non-standard RSA exponent found: {}", keyType, e); + } + + BigInteger d = KeyEntryResolver.decodeBigInt(keyData); + BigInteger inverseQmodP = KeyEntryResolver.decodeBigInt(keyData); + Objects.requireNonNull(inverseQmodP, "Missing iqmodp"); // TODO run some validation on it + BigInteger p = KeyEntryResolver.decodeBigInt(keyData); + BigInteger q = KeyEntryResolver.decodeBigInt(keyData); + BigInteger modulus = p.multiply(q); + if (!Objects.equals(n, modulus)) { + log.warn("decodePrivateKey({}) mismatched modulus values: encoded={}, calculated={}", + keyType, n, modulus); + } + + return generatePrivateKey(new RSAPrivateKeySpec(n, d)); + } + + + @Override + public boolean isPublicKeyRecoverySupported() { + return true; + } + + @Override + public RSAPublicKey recoverPublicKey(RSAPrivateKey privateKey) throws GeneralSecurityException { + if (privateKey instanceof RSAPrivateCrtKey) { + return recoverFromRSAPrivateCrtKey((RSAPrivateCrtKey) privateKey); + } else { + return recoverPublicKey(privateKey.getModulus(), privateKey.getPrivateExponent()); + } + } + + protected RSAPublicKey recoverPublicKey(BigInteger modulus, BigInteger privateExponent) throws GeneralSecurityException { + return generatePublicKey(new RSAPublicKeySpec(modulus, DEFAULT_PUBLIC_EXPONENT)); + } + + protected RSAPublicKey recoverFromRSAPrivateCrtKey(RSAPrivateCrtKey rsaKey) throws GeneralSecurityException { + BigInteger p = rsaKey.getPrimeP(); + BigInteger q = rsaKey.getPrimeQ(); + BigInteger n = p.multiply(q); + BigInteger e = rsaKey.getPublicExponent(); + return generatePublicKey(new RSAPublicKeySpec(n, e)); + } + + @Override + public RSAPublicKey clonePublicKey(RSAPublicKey key) throws GeneralSecurityException { + if (key == null) { + return null; + } else { + return generatePublicKey(new RSAPublicKeySpec(key.getModulus(), key.getPublicExponent())); + } + } + + @Override + public RSAPrivateKey clonePrivateKey(RSAPrivateKey key) throws GeneralSecurityException { + if (key == null) { + return null; + } + + if (!(key instanceof RSAPrivateCrtKey)) { + throw new InvalidKeyException("Cannot clone a non-RSAPrivateCrtKey: " + key.getClass().getSimpleName()); + } + + RSAPrivateCrtKey rsaPrv = (RSAPrivateCrtKey) key; + return generatePrivateKey( + new RSAPrivateCrtKeySpec( + rsaPrv.getModulus(), + rsaPrv.getPublicExponent(), + rsaPrv.getPrivateExponent(), + rsaPrv.getPrimeP(), + rsaPrv.getPrimeQ(), + rsaPrv.getPrimeExponentP(), + rsaPrv.getPrimeExponentQ(), + rsaPrv.getCrtCoefficient())); + } + + @Override + public KeyPairGenerator getKeyPairGenerator() throws GeneralSecurityException { + return SecurityUtils.getKeyPairGenerator(KeyUtils.RSA_ALGORITHM); + } + + @Override + public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException { + return SecurityUtils.getKeyFactory(KeyUtils.RSA_ALGORITHM); + } +} http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/0d7af8c8/sshd-core/src/main/java/org/apache/sshd/common/kex/ECDH.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/kex/ECDH.java b/sshd-core/src/main/java/org/apache/sshd/common/kex/ECDH.java index a02d811..2f7170c 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/kex/ECDH.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/kex/ECDH.java @@ -31,8 +31,8 @@ import java.util.Objects; import javax.crypto.KeyAgreement; import org.apache.sshd.common.cipher.ECCurves; -import org.apache.sshd.common.config.keys.ECDSAPublicKeyEntryDecoder; import org.apache.sshd.common.config.keys.KeyUtils; +import org.apache.sshd.common.config.keys.impl.ECDSAPublicKeyEntryDecoder; import org.apache.sshd.common.digest.Digest; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.security.SecurityUtils;
