You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2018/09/06 16:03:54 UTC
[44/51] [abbrv] mina-sshd git commit: [SSHD-842] Split common
utilities code from sshd-core into sshd-common (new artifact)
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java
new file mode 100644
index 0000000..397a007
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java
@@ -0,0 +1,178 @@
+/*
+ * 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.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.Objects;
+
+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.buffer.BufferUtils;
+import org.apache.sshd.common.util.security.SecurityUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">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.isECCSupported()) {
+ 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[] octets = KeyEntryResolver.readRLEBytes(keyData);
+ ECPoint w;
+ try {
+ w = ECCurves.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());
+ }
+
+ ECParameterSpec paramSpec = curve.getParameters();
+ return generatePublicKey(new ECPublicKeySpec(w, paramSpec));
+ }
+
+ @Override
+ public ECPublicKey clonePublicKey(ECPublicKey key) throws GeneralSecurityException {
+ if (!SecurityUtils.isECCSupported()) {
+ 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.isECCSupported()) {
+ 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);
+ ECCurves.ECPointCompression.UNCOMPRESSED.writeECPoint(s, curveName, key.getW());
+ return keyType;
+ }
+
+ @Override
+ public KeyFactory getKeyFactoryInstance() throws GeneralSecurityException {
+ if (SecurityUtils.isECCSupported()) {
+ 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.isECCSupported()) {
+ return SecurityUtils.getKeyPairGenerator(KeyUtils.EC_ALGORITHM);
+ } else {
+ throw new NoSuchProviderException("ECC not supported");
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/RSAPublicKeyDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/RSAPublicKeyDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/impl/RSAPublicKeyDecoder.java
new file mode 100644
index 0000000..4550815
--- /dev/null
+++ b/sshd-common/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:dev@mina.apache.org">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/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AESPrivateKeyObfuscator.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AESPrivateKeyObfuscator.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AESPrivateKeyObfuscator.java
new file mode 100644
index 0000000..4437945
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AESPrivateKeyObfuscator.java
@@ -0,0 +1,110 @@
+/*
+ * 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.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.spec.InvalidKeySpecException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.sshd.common.util.security.SecurityUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class AESPrivateKeyObfuscator extends AbstractPrivateKeyObfuscator {
+ public static final String CIPHER_NAME = "AES";
+ public static final AESPrivateKeyObfuscator INSTANCE = new AESPrivateKeyObfuscator();
+
+ public AESPrivateKeyObfuscator() {
+ super(CIPHER_NAME);
+ }
+
+ @Override
+ public List<Integer> getSupportedKeySizes() {
+ return getAvailableKeyLengths();
+ }
+
+ @Override
+ public byte[] applyPrivateKeyCipher(byte[] bytes, PrivateKeyEncryptionContext encContext, boolean encryptIt) throws GeneralSecurityException {
+ int keyLength = resolveKeyLength(encContext);
+ byte[] keyValue = deriveEncryptionKey(encContext, keyLength / Byte.SIZE);
+ return applyPrivateKeyCipher(bytes, encContext, keyLength, keyValue, encryptIt);
+ }
+
+ @Override
+ protected int resolveKeyLength(PrivateKeyEncryptionContext encContext) throws GeneralSecurityException {
+ String cipherType = encContext.getCipherType();
+ try {
+ int keyLength = Integer.parseInt(cipherType);
+ List<Integer> sizes = getSupportedKeySizes();
+ for (Integer s : sizes) {
+ if (s.intValue() == keyLength) {
+ return keyLength;
+ }
+ }
+
+ throw new InvalidKeySpecException("Unknown " + getCipherName() + " key length: " + cipherType + " - supported: " + sizes);
+ } catch (NumberFormatException e) {
+ throw new InvalidKeySpecException("Bad " + getCipherName() + " key length (" + cipherType + "): " + e.getMessage(), e);
+ }
+ }
+
+ /**
+ * @return A {@link List} of {@link Integer}s holding the available key
+ * lengths values (in bits) for the JVM. <B>Note:</B> AES 256 requires
+ * special JCE policy extension installation (e.g., for Java 7 see
+ * <A HREF="http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html">this link</A>)
+ */
+ @SuppressWarnings("synthetic-access")
+ public static List<Integer> getAvailableKeyLengths() {
+ return LazyKeyLengthsHolder.KEY_LENGTHS;
+ }
+
+ private static final class LazyKeyLengthsHolder {
+ private static final List<Integer> KEY_LENGTHS = Collections.unmodifiableList(detectSupportedKeySizes());
+
+ private LazyKeyLengthsHolder() {
+ throw new UnsupportedOperationException("No instance allowed");
+ }
+
+ // AES 256 requires special JCE policy extension installation
+ private static List<Integer> detectSupportedKeySizes() {
+ List<Integer> sizes = new ArrayList<>();
+ for (int keyLength = 128; keyLength < Short.MAX_VALUE /* just so it doesn't go forever */; keyLength += 64) {
+ try {
+ byte[] keyAsBytes = new byte[keyLength / Byte.SIZE];
+ Key key = new SecretKeySpec(keyAsBytes, CIPHER_NAME);
+ Cipher c = SecurityUtils.getCipher(CIPHER_NAME);
+ c.init(Cipher.DECRYPT_MODE, key);
+ sizes.add(Integer.valueOf(keyLength));
+ } catch (GeneralSecurityException e) {
+ return sizes;
+ }
+ }
+
+ throw new IllegalStateException("No limit encountered: " + sizes);
+ }
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractKeyPairResourceParser.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractKeyPairResourceParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractKeyPairResourceParser.java
new file mode 100644
index 0000000..a83bf68
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractKeyPairResourceParser.java
@@ -0,0 +1,181 @@
+/*
+ * 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.Map;
+import java.util.logging.Level;
+
+import org.apache.sshd.common.config.keys.FilePasswordProvider;
+import org.apache.sshd.common.util.GenericUtils;
+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:dev@mina.apache.org">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 (Map.Entry<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 {
+ return extractKeyPairs(resourceKey, beginMarker, endMarker, passwordProvider, KeyPairResourceParser.extractDataBytes(lines));
+ }
+
+ /**
+ * @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/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractPrivateKeyObfuscator.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractPrivateKeyObfuscator.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractPrivateKeyObfuscator.java
new file mode 100644
index 0000000..ac93ab4
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractPrivateKeyObfuscator.java
@@ -0,0 +1,191 @@
+/*
+ * 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.ByteArrayOutputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+import java.security.GeneralSecurityException;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.security.spec.InvalidKeySpecException;
+import java.util.Arrays;
+import java.util.Objects;
+import java.util.Random;
+
+import javax.crypto.Cipher;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+
+import org.apache.sshd.common.digest.BuiltinDigests;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+import org.apache.sshd.common.util.buffer.BufferUtils;
+import org.apache.sshd.common.util.security.SecurityUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public abstract class AbstractPrivateKeyObfuscator implements PrivateKeyObfuscator {
+ private final String algName;
+
+ protected AbstractPrivateKeyObfuscator(String name) {
+ algName = ValidateUtils.checkNotNullAndNotEmpty(name, "No name specified");
+ }
+
+ @Override
+ public final String getCipherName() {
+ return algName;
+ }
+
+ @Override
+ public byte[] generateInitializationVector(PrivateKeyEncryptionContext encContext) throws GeneralSecurityException {
+ return generateInitializationVector(resolveKeyLength(encContext));
+ }
+
+ @Override
+ public <A extends Appendable> A appendPrivateKeyEncryptionContext(A sb, PrivateKeyEncryptionContext encContext) throws IOException {
+ if (encContext == null) {
+ return sb;
+ }
+
+ sb.append("DEK-Info: ").append(encContext.getCipherName())
+ .append('-').append(encContext.getCipherType())
+ .append('-').append(encContext.getCipherMode());
+
+ byte[] initVector = encContext.getInitVector();
+ Objects.requireNonNull(initVector, "No encryption init vector");
+ ValidateUtils.checkTrue(initVector.length > 0, "Empty encryption init vector");
+ BufferUtils.appendHex(sb.append(','), BufferUtils.EMPTY_HEX_SEPARATOR, initVector);
+ sb.append(System.lineSeparator());
+ return sb;
+ }
+
+ protected byte[] generateInitializationVector(int keyLength) {
+ int keySize = keyLength / Byte.SIZE;
+ if ((keyLength % Byte.SIZE) != 0) { // e.g., if 36-bits then we need 5 bytes to hold
+ keySize++;
+ }
+
+ byte[] initVector = new byte[keySize];
+ Random randomizer = new SecureRandom(); // TODO consider using some pre-created singleton instance
+ randomizer.nextBytes(initVector);
+ return initVector;
+ }
+
+ protected abstract int resolveKeyLength(PrivateKeyEncryptionContext encContext) throws GeneralSecurityException;
+
+ // see http://martin.kleppmann.com/2013/05/24/improving-security-of-ssh-private-keys.html
+ // see http://www.ict.griffith.edu.au/anthony/info/crypto/openssl.hints (Password to Encryption Key section)
+ // see http://openssl.6102.n7.nabble.com/DES-EDE3-CBC-technical-details-td24883.html
+ protected byte[] deriveEncryptionKey(PrivateKeyEncryptionContext encContext, int outputKeyLength) throws GeneralSecurityException {
+ Objects.requireNonNull(encContext, "No encryption context");
+ ValidateUtils.checkNotNullAndNotEmpty(encContext.getCipherName(), "No cipher name");
+ ValidateUtils.checkNotNullAndNotEmpty(encContext.getCipherType(), "No cipher type");
+ ValidateUtils.checkNotNullAndNotEmpty(encContext.getCipherMode(), "No cipher mode");
+
+ byte[] initVector = Objects.requireNonNull(encContext.getInitVector(), "No encryption init vector");
+ ValidateUtils.checkTrue(initVector.length > 0, "Empty encryption init vector");
+
+ String password = ValidateUtils.checkNotNullAndNotEmpty(encContext.getPassword(), "No encryption password");
+ byte[] passBytes = password.getBytes(StandardCharsets.UTF_8);
+ byte[] keyValue = new byte[outputKeyLength];
+ MessageDigest hash = SecurityUtils.getMessageDigest(BuiltinDigests.Constants.MD5);
+ byte[] prevHash = GenericUtils.EMPTY_BYTE_ARRAY;
+ for (int index = 0, remLen = keyValue.length; index < keyValue.length;) {
+ hash.reset(); // just making sure
+
+ hash.update(prevHash, 0, prevHash.length);
+ hash.update(passBytes, 0, passBytes.length);
+ hash.update(initVector, 0, Math.min(initVector.length, 8));
+
+ prevHash = hash.digest();
+
+ System.arraycopy(prevHash, 0, keyValue, index, Math.min(remLen, prevHash.length));
+ index += prevHash.length;
+ remLen -= prevHash.length;
+ }
+
+ return keyValue;
+ }
+
+ protected byte[] applyPrivateKeyCipher(byte[] bytes, PrivateKeyEncryptionContext encContext, int numBits, byte[] keyValue, boolean encryptIt)
+ throws GeneralSecurityException {
+ Objects.requireNonNull(encContext, "No encryption context");
+ String cipherName = ValidateUtils.checkNotNullAndNotEmpty(encContext.getCipherName(), "No cipher name");
+ ValidateUtils.checkNotNullAndNotEmpty(encContext.getCipherType(), "No cipher type");
+ String cipherMode = ValidateUtils.checkNotNullAndNotEmpty(encContext.getCipherMode(), "No cipher mode");
+
+ Objects.requireNonNull(bytes, "No source data");
+ Objects.requireNonNull(keyValue, "No encryption key");
+ ValidateUtils.checkTrue(keyValue.length > 0, "Empty encryption key");
+
+ byte[] initVector = Objects.requireNonNull(encContext.getInitVector(), "No encryption init vector");
+ ValidateUtils.checkTrue(initVector.length > 0, "Empty encryption init vector");
+
+ String xform = cipherName + "/" + cipherMode + "/NoPadding";
+ int maxAllowedBits = Cipher.getMaxAllowedKeyLength(xform);
+ // see http://www.javamex.com/tutorials/cryptography/unrestricted_policy_files.shtml
+ if (numBits > maxAllowedBits) {
+ throw new InvalidKeySpecException("applyPrivateKeyCipher(" + xform + ")[encrypt=" + encryptIt + "]"
+ + " required key length (" + numBits + ")"
+ + " exceeds max. available: " + maxAllowedBits);
+ }
+
+ SecretKeySpec skeySpec = new SecretKeySpec(keyValue, cipherName);
+ IvParameterSpec ivspec = new IvParameterSpec(initVector);
+ Cipher cipher = SecurityUtils.getCipher(xform);
+ int blockSize = cipher.getBlockSize();
+ int dataSize = bytes.length;
+ cipher.init(encryptIt ? Cipher.ENCRYPT_MODE : Cipher.DECRYPT_MODE, skeySpec, ivspec);
+ if (blockSize <= 0) {
+ return cipher.doFinal(bytes);
+ }
+
+ int remLen = dataSize % blockSize;
+ if (remLen <= 0) {
+ return cipher.doFinal(bytes);
+ }
+
+ int updateSize = dataSize - remLen;
+ byte[] lastBlock = new byte[blockSize];
+ Arrays.fill(lastBlock, (byte) 10);
+ System.arraycopy(bytes, updateSize, lastBlock, 0, remLen);
+
+ // TODO for some reason, calling cipher.update followed by cipher.doFinal does not work
+ ByteArrayOutputStream baos = new ByteArrayOutputStream(dataSize);
+ try {
+ try {
+ byte[] buf = cipher.update(bytes, 0, updateSize);
+ baos.write(buf);
+
+ buf = cipher.doFinal(lastBlock);
+ baos.write(buf);
+ } finally {
+ baos.close();
+ }
+ } catch (IOException e) {
+ throw new GeneralSecurityException("applyPrivateKeyCipher(" + xform + ")[encrypt=" + encryptIt + "]"
+ + " failed (" + e.getClass().getSimpleName() + ")"
+ + " to split-write: " + e.getMessage(), e);
+ }
+
+ return baos.toByteArray();
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/DESPrivateKeyObfuscator.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/DESPrivateKeyObfuscator.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/DESPrivateKeyObfuscator.java
new file mode 100644
index 0000000..2043f06
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/DESPrivateKeyObfuscator.java
@@ -0,0 +1,76 @@
+/*
+ * 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.security.GeneralSecurityException;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class DESPrivateKeyObfuscator extends AbstractPrivateKeyObfuscator {
+ public static final int DEFAULT_KEY_LENGTH = 24 /* hardwired size for 3DES */;
+ public static final List<Integer> AVAILABLE_KEY_LENGTHS =
+ Collections.unmodifiableList(Collections.singletonList(Integer.valueOf(DEFAULT_KEY_LENGTH)));
+ public static final DESPrivateKeyObfuscator INSTANCE = new DESPrivateKeyObfuscator();
+
+ public DESPrivateKeyObfuscator() {
+ super("DES");
+ }
+
+ @Override
+ public byte[] applyPrivateKeyCipher(byte[] bytes, PrivateKeyEncryptionContext encContext, boolean encryptIt) throws GeneralSecurityException {
+ PrivateKeyEncryptionContext effContext = resolveEffectiveContext(encContext);
+ byte[] keyValue = deriveEncryptionKey(effContext, DEFAULT_KEY_LENGTH);
+ return applyPrivateKeyCipher(bytes, effContext, keyValue.length * Byte.SIZE, keyValue, encryptIt);
+ }
+
+ @Override
+ public List<Integer> getSupportedKeySizes() {
+ return AVAILABLE_KEY_LENGTHS;
+ }
+
+ @Override
+ protected int resolveKeyLength(PrivateKeyEncryptionContext encContext) throws GeneralSecurityException {
+ return DEFAULT_KEY_LENGTH;
+ }
+
+ @Override
+ protected byte[] generateInitializationVector(int keyLength) {
+ return super.generateInitializationVector(8 * Byte.SIZE);
+ }
+
+ public static final PrivateKeyEncryptionContext resolveEffectiveContext(PrivateKeyEncryptionContext encContext) {
+ if (encContext == null) {
+ return null;
+ }
+
+ String cipherName = encContext.getCipherName();
+ String cipherType = encContext.getCipherType();
+ PrivateKeyEncryptionContext effContext = encContext;
+ if ("EDE3".equalsIgnoreCase(cipherType)) {
+ cipherName += "ede";
+ effContext = encContext.clone();
+ effContext.setCipherName(cipherName);
+ }
+
+ return effContext;
+ }
+}
\ No newline at end of file
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceLoader.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceLoader.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceLoader.java
new file mode 100644
index 0000000..fa6930a
--- /dev/null
+++ b/sshd-common/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:dev@mina.apache.org">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/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceParser.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceParser.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceParser.java
new file mode 100644
index 0000000..80fc2c5
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceParser.java
@@ -0,0 +1,192 @@
+/*
+ * 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.AbstractMap.SimpleImmutableEntry;
+import java.util.Arrays;
+import java.util.Base64;
+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.ValidateUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">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;
+
+ /**
+ * Converts the lines assumed to contain BASE-64 encoded data into
+ * the actual content bytes.
+ *
+ * @param lines The data lines - empty lines and spaces are automatically
+ * deleted <U>before</U> BASE-64 decoding takes place.
+ * @return The decoded data bytes
+ * @see #joinDataLines(Collection)
+ */
+ static byte[] extractDataBytes(Collection<String> lines) {
+ String data = joinDataLines(lines);
+ Base64.Decoder decoder = Base64.getDecoder();
+ return decoder.decode(data);
+ }
+
+ static String joinDataLines(Collection<String> lines) {
+ String data = GenericUtils.join(lines, ' ');
+ data = data.replaceAll("\\s", "");
+ data = data.trim();
+ return data;
+ }
+
+ 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 SimpleImmutableEntry} 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 SimpleImmutableEntry<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 SimpleImmutableEntry} whose key is the <U>first</U> line index
+ * that matched and value the matched marker index - {@code null} if no match found
+ */
+ static SimpleImmutableEntry<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 SimpleImmutableEntry<>(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/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyEncryptionContext.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyEncryptionContext.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyEncryptionContext.java
new file mode 100644
index 0000000..5e03cdb
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyEncryptionContext.java
@@ -0,0 +1,270 @@
+/*
+ * 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.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.NavigableSet;
+import java.util.Objects;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public class PrivateKeyEncryptionContext implements Cloneable {
+ public static final String DEFAULT_CIPHER_MODE = "CBC";
+
+ private static final Map<String, PrivateKeyObfuscator> OBFUSCATORS =
+ Stream.of(AESPrivateKeyObfuscator.INSTANCE, DESPrivateKeyObfuscator.INSTANCE)
+ .collect(Collectors.toMap(AbstractPrivateKeyObfuscator::getCipherName, Function.identity()));
+
+ private String cipherName;
+ private String cipherType;
+ private String cipherMode = DEFAULT_CIPHER_MODE;
+ private String password;
+ private byte[] initVector;
+ private transient PrivateKeyObfuscator obfuscator;
+
+ public PrivateKeyEncryptionContext() {
+ super();
+ }
+
+ public PrivateKeyEncryptionContext(String algInfo) {
+ parseAlgorithmInfo(algInfo);
+ }
+
+ public String getCipherName() {
+ return cipherName;
+ }
+
+ public void setCipherName(String value) {
+ cipherName = value;
+ }
+
+ public String getCipherType() {
+ return cipherType;
+ }
+
+ public void setCipherType(String value) {
+ cipherType = value;
+ }
+
+ public String getCipherMode() {
+ return cipherMode;
+ }
+
+ public void setCipherMode(String value) {
+ cipherMode = value;
+ }
+
+ public String getPassword() {
+ return password;
+ }
+
+ public void setPassword(String value) {
+ password = value;
+ }
+
+ public byte[] getInitVector() {
+ return initVector;
+ }
+
+ public void setInitVector(byte... values) {
+ initVector = values;
+ }
+
+ public PrivateKeyObfuscator getPrivateKeyObfuscator() {
+ return obfuscator;
+ }
+
+ public void setPrivateKeyObfuscator(PrivateKeyObfuscator value) {
+ obfuscator = value;
+ }
+
+ public PrivateKeyObfuscator resolvePrivateKeyObfuscator() {
+ PrivateKeyObfuscator value = getPrivateKeyObfuscator();
+ if (value != null) {
+ return value;
+ }
+
+ return getRegisteredPrivateKeyObfuscator(getCipherName());
+ }
+
+ public static PrivateKeyObfuscator registerPrivateKeyObfuscator(PrivateKeyObfuscator o) {
+ return registerPrivateKeyObfuscator(Objects.requireNonNull(o, "No instance provided").getCipherName(), o);
+ }
+
+ public static PrivateKeyObfuscator registerPrivateKeyObfuscator(String cipherName, PrivateKeyObfuscator o) {
+ ValidateUtils.checkNotNullAndNotEmpty(cipherName, "No cipher name");
+ Objects.requireNonNull(o, "No instance provided");
+
+ synchronized (OBFUSCATORS) {
+ return OBFUSCATORS.put(cipherName, o);
+ }
+ }
+
+ public static boolean unregisterPrivateKeyObfuscator(PrivateKeyObfuscator o) {
+ Objects.requireNonNull(o, "No instance provided");
+ String cipherName = o.getCipherName();
+ ValidateUtils.checkNotNullAndNotEmpty(cipherName, "No cipher name");
+
+ synchronized (OBFUSCATORS) {
+ PrivateKeyObfuscator prev = OBFUSCATORS.get(cipherName);
+ if (prev != o) {
+ return false;
+ }
+
+ OBFUSCATORS.remove(cipherName);
+ }
+
+ return true;
+ }
+
+ public static PrivateKeyObfuscator unregisterPrivateKeyObfuscator(String cipherName) {
+ ValidateUtils.checkNotNullAndNotEmpty(cipherName, "No cipher name");
+
+ synchronized (OBFUSCATORS) {
+ return OBFUSCATORS.remove(cipherName);
+ }
+ }
+
+ public static final PrivateKeyObfuscator getRegisteredPrivateKeyObfuscator(String cipherName) {
+ if (GenericUtils.isEmpty(cipherName)) {
+ return null;
+ }
+
+ synchronized (OBFUSCATORS) {
+ return OBFUSCATORS.get(cipherName);
+ }
+ }
+
+ public static final NavigableSet<String> getRegisteredPrivateKeyObfuscatorCiphers() {
+ synchronized (OBFUSCATORS) {
+ Collection<String> names = OBFUSCATORS.keySet();
+ return GenericUtils.asSortedSet(String.CASE_INSENSITIVE_ORDER, names);
+ }
+ }
+
+ public static final List<PrivateKeyObfuscator> getRegisteredPrivateKeyObfuscators() {
+ synchronized (OBFUSCATORS) {
+ Collection<? extends PrivateKeyObfuscator> l = OBFUSCATORS.values();
+ if (GenericUtils.isEmpty(l)) {
+ return Collections.emptyList();
+ } else {
+ return new ArrayList<>(l);
+ }
+ }
+ }
+
+ /**
+ * @param algInfo The algorithm info - format: <I>{@code name-type-mode}</I>
+ * @return The updated context instance
+ * @see #parseAlgorithmInfo(PrivateKeyEncryptionContext, String)
+ */
+ public PrivateKeyEncryptionContext parseAlgorithmInfo(String algInfo) {
+ return parseAlgorithmInfo(this, algInfo);
+ }
+
+ @Override
+ public PrivateKeyEncryptionContext clone() {
+ try {
+ PrivateKeyEncryptionContext copy = getClass().cast(super.clone());
+ byte[] v = copy.getInitVector();
+ if (v != null) {
+ v = v.clone();
+ copy.setInitVector(v);
+ }
+ return copy;
+ } catch (CloneNotSupportedException e) { // unexpected
+ throw new RuntimeException("Failed to clone: " + toString());
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return GenericUtils.hashCode(getCipherName(), Boolean.TRUE)
+ + GenericUtils.hashCode(getCipherType(), Boolean.TRUE)
+ + GenericUtils.hashCode(getCipherMode(), Boolean.TRUE)
+ + Objects.hashCode(getPassword())
+ + Arrays.hashCode(getInitVector());
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (this == obj) {
+ return true;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+
+ PrivateKeyEncryptionContext other = (PrivateKeyEncryptionContext) obj;
+ return (GenericUtils.safeCompare(getCipherName(), other.getCipherName(), false) == 0)
+ && (GenericUtils.safeCompare(getCipherType(), other.getCipherType(), false) == 0)
+ && (GenericUtils.safeCompare(getCipherMode(), other.getCipherMode(), false) == 0)
+ && (GenericUtils.safeCompare(getPassword(), other.getPassword(), true) == 0)
+ && Arrays.equals(getInitVector(), other.getInitVector());
+ }
+
+ @Override
+ public String toString() {
+ return GenericUtils.join(new String[]{getCipherName(), getCipherType(), getCipherMode()}, '-');
+ }
+
+ /**
+ * @param <C> Generic context type
+ * @param context The {@link PrivateKeyEncryptionContext} to update
+ * @param algInfo The algorithm info - format: {@code <I>name</I>-<I>type</I>-<I>mode</I>}
+ * @return The updated context
+ */
+ public static final <C extends PrivateKeyEncryptionContext> C parseAlgorithmInfo(C context, String algInfo) {
+ ValidateUtils.checkNotNullAndNotEmpty(algInfo, "No encryption algorithm data");
+
+ String[] cipherData = GenericUtils.split(algInfo, '-');
+ ValidateUtils.checkTrue(cipherData.length == 3, "Bad encryption algorithm data: %s", algInfo);
+
+ context.setCipherName(cipherData[0]);
+ context.setCipherType(cipherData[1]);
+ context.setCipherMode(cipherData[2]);
+ return context;
+ }
+
+ public static final PrivateKeyEncryptionContext newPrivateKeyEncryptionContext(PrivateKeyObfuscator o, String password) {
+ return initializeObfuscator(new PrivateKeyEncryptionContext(), o, password);
+ }
+
+ public static final <C extends PrivateKeyEncryptionContext> C initializeObfuscator(C context, PrivateKeyObfuscator o, String password) {
+ context.setCipherName(o.getCipherName());
+ context.setPrivateKeyObfuscator(o);
+ context.setPassword(password);
+ return context;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyObfuscator.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyObfuscator.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyObfuscator.java
new file mode 100644
index 0000000..d8d2db5
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyObfuscator.java
@@ -0,0 +1,64 @@
+/*
+ * 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.util.List;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+public interface PrivateKeyObfuscator {
+ /**
+ * @return Basic cipher used to obfuscate
+ */
+ String getCipherName();
+
+ /**
+ * @return A {@link List} of the supported key sizes - <B>Note:</B> every
+ * call returns a and <U>un-modifiable</U> instance.
+ */
+ List<Integer> getSupportedKeySizes();
+
+ /**
+ * @param <A> Appendable generic type
+ * @param sb The {@link Appendable} instance to update
+ * @param encContext
+ * @return Same appendable instance
+ * @throws IOException
+ */
+ <A extends Appendable> A appendPrivateKeyEncryptionContext(A sb, PrivateKeyEncryptionContext encContext) throws IOException;
+
+ /**
+ * @param encContext The encryption context
+ * @return An initialization vector suitable to the specified context
+ * @throws GeneralSecurityException
+ */
+ byte[] generateInitializationVector(PrivateKeyEncryptionContext encContext) throws GeneralSecurityException;
+
+ /**
+ * @param bytes Original bytes
+ * @param encContext The encryption context
+ * @param encryptIt If {@code true} then encrypt the original bytes, otherwise decrypt them
+ * @return The result of applying the cipher to the original bytes
+ * @throws GeneralSecurityException If cannot encrypt/decrypt
+ */
+ byte[] applyPrivateKeyCipher(byte[] bytes, PrivateKeyEncryptionContext encContext, boolean encryptIt) throws GeneralSecurityException;
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHDSSPrivateKeyEntryDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHDSSPrivateKeyEntryDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHDSSPrivateKeyEntryDecoder.java
new file mode 100644
index 0000000..6188a04
--- /dev/null
+++ b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHDSSPrivateKeyEntryDecoder.java
@@ -0,0 +1,139 @@
+/*
+ * 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:dev@mina.apache.org">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 {
+ return KeyUtils.recoverDSAPublicKey(privateKey);
+ }
+
+ @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/10de190e/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHECDSAPrivateKeyEntryDecoder.java
----------------------------------------------------------------------
diff --git a/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHECDSAPrivateKeyEntryDecoder.java b/sshd-common/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHECDSAPrivateKeyEntryDecoder.java
new file mode 100644
index 0000000..bceca59
--- /dev/null
+++ b/sshd-common/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:dev@mina.apache.org">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.isECCSupported()) {
+ 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.isECCSupported()) {
+ 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.isECCSupported()) {
+ 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.isECCSupported()) {
+ 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.isECCSupported()) {
+ return SecurityUtils.getKeyPairGenerator(KeyUtils.EC_ALGORITHM);
+ } else {
+ throw new NoSuchProviderException("ECC not supported");
+ }
+ }
+}