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:[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.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:[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/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:[email protected]">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:[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 (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:[email protected]">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:[email protected]">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:[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/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:[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; + + /** + * 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:[email protected]">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:[email protected]">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:[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 { + 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:[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.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"); + } + } +}
