[SSHD-500] Implement loading a KeyPair file directly without BouncyCastle help
Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/e767438a Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/e767438a Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/e767438a Branch: refs/heads/master Commit: e767438ae47f579bf001a3fbb47c6b1a4df13b1c Parents: d13c679 Author: Lyor Goldstein <lyor.goldst...@gmail.com> Authored: Fri Nov 25 17:38:11 2016 +0200 Committer: Lyor Goldstein <lyor.goldst...@gmail.com> Committed: Fri Nov 25 17:38:11 2016 +0200 ---------------------------------------------------------------------- README.md | 12 +- .../sshd/client/config/keys/ClientIdentity.java | 2 +- .../config/keys/ClientIdentityLoader.java | 14 +- .../org/apache/sshd/common/cipher/ECCurves.java | 265 ++++++++++++++- .../sshd/common/config/keys/KeyUtils.java | 117 +++++-- .../keys/impl/ECDSAPublicKeyEntryDecoder.java | 208 +----------- .../keys/loader/AESPrivateKeyObfuscator.java | 107 ++++++ .../loader/AbstractKeyPairResourceParser.java | 8 +- .../loader/AbstractPrivateKeyObfuscator.java | 191 +++++++++++ .../keys/loader/DESPrivateKeyObfuscator.java | 76 +++++ .../keys/loader/KeyPairResourceParser.java | 18 + .../loader/PrivateKeyEncryptionContext.java | 276 +++++++++++++++ .../keys/loader/PrivateKeyObfuscator.java | 64 ++++ .../OpenSSHDSSPrivateKeyEntryDecoder.java | 9 +- .../openssh/OpenSSHKeyPairResourceParser.java | 1 + .../openssh/OpenSSHRSAPrivateKeyDecoder.java | 19 +- .../pem/AbstractPEMResourceKeyPairParser.java | 168 +++++++++ .../loader/pem/DSSPEMResourceKeyPairParser.java | 127 +++++++ .../pem/ECDSAPEMResourceKeyPairParser.java | 219 ++++++++++++ .../loader/pem/KeyPairPEMResourceParser.java | 37 ++ .../keys/loader/pem/PEMResourceParserUtils.java | 109 ++++++ .../pem/PKCS8PEMResourceKeyPairParser.java | 157 +++++++++ .../loader/pem/RSAPEMResourceKeyPairParser.java | 137 ++++++++ .../java/org/apache/sshd/common/kex/ECDH.java | 3 +- .../AbstractResourceKeyPairProvider.java | 1 - .../sshd/common/signature/SignatureDSA.java | 4 +- .../sshd/common/signature/SignatureECDSA.java | 4 +- .../apache/sshd/common/util/GenericUtils.java | 34 ++ .../apache/sshd/common/util/NumberUtils.java | 41 ++- .../apache/sshd/common/util/buffer/Buffer.java | 3 +- .../sshd/common/util/buffer/BufferUtils.java | 4 + .../buffer/keys/ECBufferPublicKeyParser.java | 3 +- .../apache/sshd/common/util/io/DERParser.java | 136 -------- .../apache/sshd/common/util/io/DERWriter.java | 118 ------- .../sshd/common/util/io/der/ASN1Class.java | 93 +++++ .../sshd/common/util/io/der/ASN1Object.java | 338 +++++++++++++++++++ .../sshd/common/util/io/der/ASN1Type.java | 118 +++++++ .../sshd/common/util/io/der/DERParser.java | 151 +++++++++ .../sshd/common/util/io/der/DERWriter.java | 147 ++++++++ .../common/util/security/SecurityUtils.java | 30 +- .../security/eddsa/EdDSASecurityProvider.java | 13 + .../OpenSSHEd25519PrivateKeyEntryDecoder.java | 8 +- .../sshd/server/config/keys/ServerIdentity.java | 2 +- .../client/config/keys/ClientIdentityTest.java | 4 - .../loader/AESPrivateKeyObfuscatorTest.java | 73 ++++ .../sshd/common/util/SecurityUtilsTest.java | 47 ++- .../sshd/common/util/io/der/ASN1ClassTest.java | 61 ++++ .../sshd/common/util/io/der/ASN1TypeTest.java | 61 ++++ .../sshd/common/util/io/der/DERParserTest.java | 58 ++++ .../server/config/keys/ServerIdentityTest.java | 4 - 50 files changed, 3290 insertions(+), 610 deletions(-) ---------------------------------------------------------------------- http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e767438a/README.md ---------------------------------------------------------------------- diff --git a/README.md b/README.md index 16b0bde..bf8b2b8 100644 --- a/README.md +++ b/README.md @@ -17,9 +17,12 @@ The code only requires the core abstract [slf4j-api](https://mvnrepository.com/a * [Bouncy Castle](https://www.bouncycastle.org/) -Required only for reading/writing keys from/to PEM files or for special keys/ciphers/etc. that are not part of the standard [Java Cryptography Extension](https://en.wikipedia.org/wiki/Java_Cryptography_Extension). See [Java Cryptography Architecture (JCA) Reference Guide](https://docs.oracle.com/javase/8/docs/technotes/guides/security/crypto/CryptoSpec.html) for key classes and explanations as to how _Bouncy Castle_ is plugged in (other security providers). **Note:** the required Maven module(s) are defined as `optional` so must be added as an -**explicit** dependency in order to be included in the classpath: +Required mainly for writing keys from/to PEM files or for special keys/ciphers/etc. that are not part of the standard [Java Cryptography Extension](https://en.wikipedia.org/wiki/Java_Cryptography_Extension). See [Java Cryptography Architecture (JCA) Reference Guide](https://docs.oracle.com/javase/8/docs/technotes/guides/security/crypto/CryptoSpec.html) for key classes and explanations as to how _Bouncy Castle_ is plugged in (other security providers). + +**Caveat**: If _Bouncy Castle_ modules are registered, then the code will use its implementation of the ciphers, keys, signatures, etc. rather than the default JCE provided in the JVM. Furthermore, one can use the `BouncyCastleKeyPairResourceParser.INSTANCE` to load standard PEM files instead of the core one - either directly or via `SecurityUtils#setKeyPairResourceParser` for **global** usage. + **Note:** the required Maven module(s) are defined as `optional` so must be added as an +**explicit** dependency in order to be included in the classpath: ```xml @@ -34,9 +37,6 @@ Required only for reading/writing keys from/to PEM files or for special keys/cip ``` -**Caveat**: If _Bouncy Castle_ modules are available, then the code will use its implementation of the ciphers, keys, signatures, etc. rather than the default JCE provided in the JVM. - - * [MINA core](https://mina.apache.org/mina-project/) @@ -120,7 +120,7 @@ While RFC-4256 support is the primary purpose of this interface, it can also be ## Using the `SshClient` to connect to a server -Once the `SshClient` instance is properly configured it needs to be `start()`-ed in order to connect to a server. **Note:** one can use a single `SshClient` instance to connnect to multiple server as well as modifying the default configuration (ciphers, MACs, keys, etc.) on a per-session manner (see more in the *Advanced usage* section). Furthermore, one can change almost any configured `SshClient` parameter - although its influence on currently established sessions depends on the actual changed configuration. Here is how a typical usage would look like +Once the `SshClient` instance is properly configured it needs to be `start()`-ed in order to connect to a server. **Note:** one can use a single `SshClient` instance to connnect to multiple servers as well as modifying the default configuration (ciphers, MACs, keys, etc.) on a per-session manner (see more in the *Advanced usage* section). Furthermore, one can change almost any configured `SshClient` parameter - although its influence on currently established sessions depends on the actual changed configuration. Here is how a typical usage would look like ```java http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e767438a/sshd-core/src/main/java/org/apache/sshd/client/config/keys/ClientIdentity.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/keys/ClientIdentity.java b/sshd-core/src/main/java/org/apache/sshd/client/config/keys/ClientIdentity.java index 5802b2a..9060fec 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/config/keys/ClientIdentity.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/config/keys/ClientIdentity.java @@ -48,7 +48,7 @@ import org.apache.sshd.common.util.io.IoUtils; * Provides keys loading capability from the user's keys folder - e.g., {@code id_rsa} * * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> - * @see org.apache.sshd.common.util.security.SecurityUtils#isBouncyCastleRegistered() + * @see org.apache.sshd.common.util.security.SecurityUtils#getKeyPairResourceParser() */ public final class ClientIdentity { http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e767438a/sshd-core/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java b/sshd-core/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java index 1f04e6f..8b3a295 100644 --- a/sshd-core/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java +++ b/sshd-core/src/main/java/org/apache/sshd/client/config/keys/ClientIdentityLoader.java @@ -19,11 +19,11 @@ package org.apache.sshd.client.config.keys; -import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.security.GeneralSecurityException; import java.security.KeyPair; @@ -41,17 +41,11 @@ public interface ClientIdentityLoader { * * <P> * <B>Note:</B> It calls {@link SecurityUtils#loadKeyPairIdentity(String, InputStream, FilePasswordProvider)} - * which fails if the {@code Bouncycastle} provider is not registered, therefore the - * default {@link #isValidLocation(String)} implementation also checks if - * {@link SecurityUtils#isBouncyCastleRegistered()} * </P> */ ClientIdentityLoader DEFAULT = new ClientIdentityLoader() { @Override public boolean isValidLocation(String location) throws IOException { - if (!SecurityUtils.isBouncyCastleRegistered()) { - return false; - } Path path = toPath(location); return Files.exists(path, IoUtils.EMPTY_LINK_OPTIONS); } @@ -70,8 +64,10 @@ public interface ClientIdentityLoader { } private Path toPath(String location) { - Path path = new File(ValidateUtils.checkNotNullAndNotEmpty(location, "No location")).toPath(); - return path.toAbsolutePath().normalize(); + Path path = Paths.get(ValidateUtils.checkNotNullAndNotEmpty(location, "No location")); + path = path.toAbsolutePath(); + path = path.normalize(); + return path; } }; http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e767438a/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java b/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java index 0e99ec0..8da9f9c 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/cipher/ECCurves.java @@ -18,6 +18,11 @@ */ package org.apache.sshd.common.cipher; +import java.io.ByteArrayOutputStream; +import java.io.EOFException; +import java.io.IOException; +import java.io.OutputStream; +import java.io.StreamCorruptedException; import java.math.BigInteger; import java.security.interfaces.ECKey; import java.security.spec.ECField; @@ -35,10 +40,12 @@ import java.util.stream.Collectors; import org.apache.sshd.common.NamedResource; import org.apache.sshd.common.OptionalFeature; +import org.apache.sshd.common.config.keys.KeyEntryResolver; import org.apache.sshd.common.digest.BuiltinDigests; import org.apache.sshd.common.digest.Digest; import org.apache.sshd.common.digest.DigestFactory; import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.NumberUtils; import org.apache.sshd.common.util.ValidateUtils; import org.apache.sshd.common.util.security.SecurityUtils; @@ -48,7 +55,7 @@ import org.apache.sshd.common.util.security.SecurityUtils; * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> */ public enum ECCurves implements NamedResource, OptionalFeature { - nistp256(Constants.NISTP256, + nistp256(Constants.NISTP256, new int[]{1, 2, 840, 10045, 3, 1, 7}, new ECParameterSpec( new EllipticCurve( new ECFieldFp(new BigInteger("FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF", 16)), @@ -61,7 +68,7 @@ public enum ECCurves implements NamedResource, OptionalFeature { 1), 32, BuiltinDigests.sha256), - nistp384(Constants.NISTP384, + nistp384(Constants.NISTP384, new int[]{1, 3, 132, 0, 34}, new ECParameterSpec( new EllipticCurve( new ECFieldFp(new BigInteger("FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF", 16)), @@ -74,7 +81,7 @@ public enum ECCurves implements NamedResource, OptionalFeature { 1), 48, BuiltinDigests.sha384), - nistp521(Constants.NISTP521, + nistp521(Constants.NISTP521, new int[]{1, 3, 132, 0, 35}, new ECParameterSpec( new EllipticCurve( new ECFieldFp(new BigInteger("01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF" @@ -131,13 +138,17 @@ public enum ECCurves implements NamedResource, OptionalFeature { private final String name; private final String keyType; + private final String oidString; + private final List<Integer> oidValue; private final ECParameterSpec params; private final int keySize; private final int numOctets; private final DigestFactory digestFactory; - ECCurves(String name, ECParameterSpec params, int numOctets, DigestFactory digestFactory) { + ECCurves(String name, int[] oid, ECParameterSpec params, int numOctets, DigestFactory digestFactory) { this.name = ValidateUtils.checkNotNullAndNotEmpty(name, "No curve name"); + this.oidString = NumberUtils.join('.', ValidateUtils.checkNotNullAndNotEmpty(oid, "No OID")); + this.oidValue = Collections.unmodifiableList(NumberUtils.asList(oid)); this.keyType = Constants.ECDSA_SHA2_PREFIX + name; this.params = ValidateUtils.checkNotNull(params, "No EC params for %s", name); this.keySize = getCurveSize(params); @@ -150,6 +161,14 @@ public enum ECCurves implements NamedResource, OptionalFeature { return name; } + public final String getOID() { + return oidString; + } + + public final List<Integer> getOIDValue() { + return oidValue; + } + /** * @return The standard key type used to represent this curve */ @@ -258,6 +277,49 @@ public enum ECCurves implements NamedResource, OptionalFeature { return null; } + public static ECCurves fromOIDValue(List<? extends Number> oid) { + if (GenericUtils.isEmpty(oid)) { + return null; + } + + for (ECCurves c : VALUES) { + List<? extends Number> v = c.getOIDValue(); + if (oid.size() != v.size()) { + continue; + } + + boolean matches = true; + for (int index = 0; index < v.size(); index++) { + Number exp = v.get(index); + Number act = oid.get(index); + if (exp.intValue() != act.intValue()) { + matches = false; + break; + } + } + + if (matches) { + return c; + } + } + + return null; + } + + public static ECCurves fromOID(String oid) { + if (GenericUtils.isEmpty(oid)) { + return null; + } + + for (ECCurves c : VALUES) { + if (oid.equalsIgnoreCase(c.getOID())) { + return c; + } + } + + return null; + } + /** * @param params The curve's {@link ECParameterSpec} * @return The curve's key size in bits @@ -305,6 +367,59 @@ public enum ECCurves implements NamedResource, OptionalFeature { return output; } + /** + * Converts the given octet string (defined by ASN.1 specifications) to a {@link BigInteger} + * As octet strings always represent positive integers, a zero-byte is prepended to + * the given array if necessary (if is MSB equal to 1), then this is converted to BigInteger + * The conversion is defined in the Section 2.3.8 + * + * @param octets - octet string bytes to be converted + * @return The {@link BigInteger} representation of the octet string + */ + public static BigInteger octetStringToInteger(byte... octets) { + if (octets == null) { + return null; + } else if (octets.length == 0) { + return BigInteger.ZERO; + } else { + return new BigInteger(1, octets); + } + } + + public static ECPoint octetStringToEcPoint(byte... octets) { + if (NumberUtils.isEmpty(octets)) { + return null; + } + + int startIndex = findFirstNonZeroIndex(octets); + if (startIndex < 0) { + throw new IllegalArgumentException("All zeroes ECPoint N/A"); + } + + byte indicator = octets[startIndex]; + ECCurves.ECPointCompression compression = ECCurves.ECPointCompression.fromIndicatorValue(indicator); + if (compression == null) { + throw new UnsupportedOperationException("Unknown compression indicator value: 0x" + Integer.toHexString(indicator & 0xFF)); + } + + // The coordinates actually start after the compression indicator + return compression.octetStringToEcPoint(octets, startIndex + 1, octets.length - startIndex - 1); + } + + private static int findFirstNonZeroIndex(byte... octets) { + if (NumberUtils.isEmpty(octets)) { + return -1; + } + + for (int index = 0; index < octets.length; index++) { + if (octets[index] != 0) { + return index; + } + } + + return -1; // all zeroes + } + public static final class Constants { /** * Standard prefix of NISTP key types when encoded @@ -315,4 +430,146 @@ public enum ECCurves implements NamedResource, OptionalFeature { public static final String NISTP384 = "nistp384"; public static final String NISTP521 = "nistp521"; } + + /** + * The various {@link ECPoint} representation compression indicators + * + * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> + * @see <A HREF="https://www.ietf.org/rfc/rfc5480.txt">RFC-5480 - section 2.2</A> + */ + public enum ECPointCompression { + // see http://tools.ietf.org/html/draft-jivsov-ecc-compact-00 + // see http://crypto.stackexchange.com/questions/8914/ecdsa-compressed-public-key-point-back-to-uncompressed-public-key-point + VARIANT2((byte) 0x02) { + @Override + public ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len) { + byte[] xp = new byte[len]; + System.arraycopy(octets, startIndex, xp, 0, len); + BigInteger x = octetStringToInteger(xp); + + // TODO derive even Y... + throw new UnsupportedOperationException("octetStringToEcPoint(" + name() + ")(X=" + x + ") compression support N/A"); + } + }, + VARIANT3((byte) 0x03) { + @Override + public ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len) { + byte[] xp = new byte[len]; + System.arraycopy(octets, startIndex, xp, 0, len); + BigInteger x = octetStringToInteger(xp); + + // TODO derive odd Y... + throw new UnsupportedOperationException("octetStringToEcPoint(" + name() + ")(X=" + x + ") compression support N/A"); + } + }, + UNCOMPRESSED((byte) 0x04) { + @Override + public ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len) { + int numElements = len / 2; /* x, y */ + if (len != (numElements * 2)) { // make sure length is not odd + throw new IllegalArgumentException("octetStringToEcPoint(" + name() + ") " + + " invalid remainder octets representation: " + + " expected=" + (2 * numElements) + ", actual=" + len); + } + + byte[] xp = new byte[numElements]; + byte[] yp = new byte[numElements]; + System.arraycopy(octets, startIndex, xp, 0, numElements); + System.arraycopy(octets, startIndex + numElements, yp, 0, numElements); + + BigInteger x = octetStringToInteger(xp); + BigInteger y = octetStringToInteger(yp); + return new ECPoint(x, y); + } + + @Override + public void writeECPoint(OutputStream s, String curveName, ECPoint p) throws IOException { + ECCurves curve = fromCurveName(curveName); + if (curve == null) { + throw new StreamCorruptedException("writeECPoint(" + name() + ")[" + curveName + "] cannot determine octets count"); + } + + int numElements = curve.getNumPointOctets(); + KeyEntryResolver.encodeInt(s, 1 /* the indicator */ + 2 * numElements); + s.write(getIndicatorValue()); + writeCoordinate(s, "X", p.getAffineX(), numElements); + writeCoordinate(s, "Y", p.getAffineY(), numElements); + } + }; + + public static final Set<ECPointCompression> VALUES = + Collections.unmodifiableSet(EnumSet.allOf(ECPointCompression.class)); + + private final byte indicatorValue; + + ECPointCompression(byte indicator) { + indicatorValue = indicator; + } + + public final byte getIndicatorValue() { + return indicatorValue; + } + + public abstract ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len); + + public byte[] ecPointToOctetString(String curveName, ECPoint p) { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream((2 * 66) + Long.SIZE)) { + writeECPoint(baos, curveName, p); + return baos.toByteArray(); + } catch (IOException e) { + throw new RuntimeException("ecPointToOctetString(" + curveName + ")" + + " failed (" + e.getClass().getSimpleName() + ")" + + " to write data: " + e.getMessage(), + e); + } + } + + public void writeECPoint(OutputStream s, String curveName, ECPoint p) throws IOException { + if (s == null) { + throw new EOFException("No output stream"); + } + + throw new StreamCorruptedException("writeECPoint(" + name() + ")[" + p + "] N/A"); + } + + protected void writeCoordinate(OutputStream s, String n, BigInteger v, int numElements) throws IOException { + byte[] vp = v.toByteArray(); + int startIndex = 0; + int vLen = vp.length; + if (vLen > numElements) { + if (vp[0] == 0) { // skip artificial positive sign + startIndex++; + vLen--; + } + } + + if (vLen > numElements) { + throw new StreamCorruptedException("writeCoordinate(" + name() + ")[" + n + "]" + + " value length (" + vLen + ") exceeds max. (" + numElements + ")" + + " for " + v); + } + + if (vLen < numElements) { + byte[] tmp = new byte[numElements]; + System.arraycopy(vp, startIndex, tmp, numElements - vLen, vLen); + vp = tmp; + } + + s.write(vp, startIndex, vLen); + } + + public static ECPointCompression fromIndicatorValue(int value) { + if ((value < 0) || (value > 0xFF)) { + return null; // must be a byte value + } + + for (ECPointCompression c : VALUES) { + if (value == c.getIndicatorValue()) { + return c; + } + } + + return null; + } + } } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e767438a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java index a4787cb..bd56559 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/KeyUtils.java @@ -28,6 +28,7 @@ import java.nio.file.Path; import java.nio.file.attribute.PosixFilePermission; import java.security.GeneralSecurityException; import java.security.Key; +import java.security.KeyFactory; import java.security.KeyPair; import java.security.PrivateKey; import java.security.PublicKey; @@ -39,10 +40,13 @@ import java.security.interfaces.ECKey; import java.security.interfaces.ECPrivateKey; import java.security.interfaces.ECPublicKey; import java.security.interfaces.RSAKey; +import java.security.interfaces.RSAPrivateCrtKey; import java.security.interfaces.RSAPrivateKey; import java.security.interfaces.RSAPublicKey; +import java.security.spec.DSAPublicKeySpec; import java.security.spec.ECParameterSpec; import java.security.spec.InvalidKeySpecException; +import java.security.spec.RSAPublicKeySpec; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -86,6 +90,11 @@ public final class KeyUtils { public static final String RSA_ALGORITHM = "RSA"; /** + * The most commonly used RSA public key exponent + */ + public static final BigInteger DEFAULT_RSA_PUBLIC_EXPONENT = new BigInteger("65537"); + + /** * Name of algorithm for DSS keys to be used when calling security provider */ public static final String DSS_ALGORITHM = "DSA"; @@ -759,6 +768,33 @@ public final class KeyUtils { } } + public static boolean compareKeys(PublicKey k1, PublicKey k2) { + if ((k1 instanceof RSAPublicKey) && (k2 instanceof RSAPublicKey)) { + return compareRSAKeys(RSAPublicKey.class.cast(k1), RSAPublicKey.class.cast(k2)); + } else if ((k1 instanceof DSAPublicKey) && (k2 instanceof DSAPublicKey)) { + return compareDSAKeys(DSAPublicKey.class.cast(k1), DSAPublicKey.class.cast(k2)); + } else if ((k1 instanceof ECPublicKey) && (k2 instanceof ECPublicKey)) { + return compareECKeys(ECPublicKey.class.cast(k1), ECPublicKey.class.cast(k2)); + } else if ((k1 != null) && SecurityUtils.EDDSA.equalsIgnoreCase(k1.getAlgorithm()) + && (k2 != null) && SecurityUtils.EDDSA.equalsIgnoreCase(k2.getAlgorithm())) { + return SecurityUtils.compareEDDSAPPublicKeys(k1, k2); + } else { + return false; // either key is null or not of same class + } + } + + public static PublicKey recoverPublicKey(PrivateKey key) throws GeneralSecurityException { + if (key instanceof RSAPrivateKey) { + return recoverRSAPublicKey((RSAPrivateKey) key); + } else if (key instanceof DSAPrivateKey) { + return recoverDSAPublicKey((DSAPrivateKey) key); + } else if ((key != null) && SecurityUtils.EDDSA.equalsIgnoreCase(key.getAlgorithm())) { + return SecurityUtils.recoverEDDSAPublicKey(key); + } else { + return null; + } + } + public static boolean compareKeys(PrivateKey k1, PrivateKey k2) { if ((k1 instanceof RSAPrivateKey) && (k2 instanceof RSAPrivateKey)) { return compareRSAKeys(RSAPrivateKey.class.cast(k1), RSAPrivateKey.class.cast(k2)); @@ -774,72 +810,68 @@ public final class KeyUtils { } } - public static boolean compareRSAKeys(RSAPrivateKey k1, RSAPrivateKey k2) { + public static boolean compareRSAKeys(RSAPublicKey k1, RSAPublicKey k2) { if (Objects.equals(k1, k2)) { return true; } else if (k1 == null || k2 == null) { return false; // both null is covered by Objects#equals } else { - return Objects.equals(k1.getModulus(), k2.getModulus()) - && Objects.equals(k1.getPrivateExponent(), k2.getPrivateExponent()); + return Objects.equals(k1.getPublicExponent(), k2.getPublicExponent()) + && Objects.equals(k1.getModulus(), k2.getModulus()); } } - public static boolean compareDSAKeys(DSAPrivateKey k1, DSAPrivateKey k2) { + public static boolean compareRSAKeys(RSAPrivateKey k1, RSAPrivateKey k2) { if (Objects.equals(k1, k2)) { return true; } else if (k1 == null || k2 == null) { return false; // both null is covered by Objects#equals } else { - return Objects.equals(k1.getX(), k2.getX()) - && compareDSAParams(k1.getParams(), k2.getParams()); + return Objects.equals(k1.getModulus(), k2.getModulus()) + && Objects.equals(k1.getPrivateExponent(), k2.getPrivateExponent()); } } - public static boolean compareECKeys(ECPrivateKey k1, ECPrivateKey k2) { - if (Objects.equals(k1, k2)) { - return true; - } else if (k1 == null || k2 == null) { - return false; // both null is covered by Objects#equals + public static RSAPublicKey recoverRSAPublicKey(RSAPrivateKey privateKey) throws GeneralSecurityException { + if (privateKey instanceof RSAPrivateCrtKey) { + return recoverFromRSAPrivateCrtKey((RSAPrivateCrtKey) privateKey); } else { - return Objects.equals(k1.getS(), k2.getS()) - && compareECParams(k1.getParams(), k2.getParams()); + // Not ideal, but best we can do under the circumstances + return recoverRSAPublicKey(privateKey.getModulus(), DEFAULT_RSA_PUBLIC_EXPONENT); } } - public static boolean compareKeys(PublicKey k1, PublicKey k2) { - if ((k1 instanceof RSAPublicKey) && (k2 instanceof RSAPublicKey)) { - return compareRSAKeys(RSAPublicKey.class.cast(k1), RSAPublicKey.class.cast(k2)); - } else if ((k1 instanceof DSAPublicKey) && (k2 instanceof DSAPublicKey)) { - return compareDSAKeys(DSAPublicKey.class.cast(k1), DSAPublicKey.class.cast(k2)); - } else if ((k1 instanceof ECPublicKey) && (k2 instanceof ECPublicKey)) { - return compareECKeys(ECPublicKey.class.cast(k1), ECPublicKey.class.cast(k2)); - } else if ((k1 != null) && SecurityUtils.EDDSA.equalsIgnoreCase(k1.getAlgorithm()) - && (k2 != null) && SecurityUtils.EDDSA.equalsIgnoreCase(k2.getAlgorithm())) { - return SecurityUtils.compareEDDSAPPublicKeys(k1, k2); - } else { - return false; // either key is null or not of same class - } + public static RSAPublicKey recoverFromRSAPrivateCrtKey(RSAPrivateCrtKey rsaKey) throws GeneralSecurityException { + return recoverRSAPublicKey(rsaKey.getPrimeP(), rsaKey.getPrimeQ(), rsaKey.getPublicExponent()); } - public static boolean compareRSAKeys(RSAPublicKey k1, RSAPublicKey k2) { + public static RSAPublicKey recoverRSAPublicKey(BigInteger p, BigInteger q, BigInteger publicExponent) throws GeneralSecurityException { + return recoverRSAPublicKey(p.multiply(q), publicExponent); + } + + public static RSAPublicKey recoverRSAPublicKey(BigInteger modulus, BigInteger publicExponent) throws GeneralSecurityException { + KeyFactory kf = SecurityUtils.getKeyFactory(RSA_ALGORITHM); + return (RSAPublicKey) kf.generatePublic(new RSAPublicKeySpec(modulus, publicExponent)); + } + + public static boolean compareDSAKeys(DSAPublicKey k1, DSAPublicKey k2) { if (Objects.equals(k1, k2)) { return true; } else if (k1 == null || k2 == null) { return false; // both null is covered by Objects#equals } else { - return Objects.equals(k1.getPublicExponent(), k2.getPublicExponent()) - && Objects.equals(k1.getModulus(), k2.getModulus()); + return Objects.equals(k1.getY(), k2.getY()) + && compareDSAParams(k1.getParams(), k2.getParams()); } } - public static boolean compareDSAKeys(DSAPublicKey k1, DSAPublicKey k2) { + public static boolean compareDSAKeys(DSAPrivateKey k1, DSAPrivateKey k2) { if (Objects.equals(k1, k2)) { return true; } else if (k1 == null || k2 == null) { return false; // both null is covered by Objects#equals } else { - return Objects.equals(k1.getY(), k2.getY()) + return Objects.equals(k1.getX(), k2.getX()) && compareDSAParams(k1.getParams(), k2.getParams()); } } @@ -856,6 +888,29 @@ public final class KeyUtils { } } + // based on code from https://github.com/alexo/SAML-2.0/blob/master/java-opensaml/opensaml-security-api/src/main/java/org/opensaml/xml/security/SecurityHelper.java + public static DSAPublicKey recoverDSAPublicKey(DSAPrivateKey privateKey) throws GeneralSecurityException { + DSAParams keyParams = privateKey.getParams(); + BigInteger p = keyParams.getP(); + BigInteger x = privateKey.getX(); + BigInteger q = keyParams.getQ(); + BigInteger g = keyParams.getG(); + BigInteger y = g.modPow(x, p); + KeyFactory kf = SecurityUtils.getKeyFactory(DSS_ALGORITHM); + return (DSAPublicKey) kf.generatePublic(new DSAPublicKeySpec(y, p, q, g)); + } + + public static boolean compareECKeys(ECPrivateKey k1, ECPrivateKey k2) { + if (Objects.equals(k1, k2)) { + return true; + } else if (k1 == null || k2 == null) { + return false; // both null is covered by Objects#equals + } else { + return Objects.equals(k1.getS(), k2.getS()) + && compareECParams(k1.getParams(), k2.getParams()); + } + } + public static boolean compareECKeys(ECPublicKey k1, ECPublicKey k2) { if (Objects.equals(k1, k2)) { return true; http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e767438a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java index d180e9e..56ff39e 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/impl/ECDSAPublicKeyEntryDecoder.java @@ -19,13 +19,9 @@ package org.apache.sshd.common.config.keys.impl; -import java.io.ByteArrayOutputStream; -import java.io.EOFException; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; -import java.io.StreamCorruptedException; -import java.math.BigInteger; import java.security.GeneralSecurityException; import java.security.InvalidKeyException; import java.security.KeyFactory; @@ -39,15 +35,11 @@ import java.security.spec.ECPoint; import java.security.spec.ECPrivateKeySpec; import java.security.spec.ECPublicKeySpec; import java.security.spec.InvalidKeySpecException; -import java.util.Collections; -import java.util.EnumSet; import java.util.Objects; -import java.util.Set; import org.apache.sshd.common.cipher.ECCurves; import org.apache.sshd.common.config.keys.KeyEntryResolver; import org.apache.sshd.common.config.keys.KeyUtils; -import org.apache.sshd.common.util.NumberUtils; import org.apache.sshd.common.util.buffer.BufferUtils; import org.apache.sshd.common.util.security.SecurityUtils; @@ -88,7 +80,7 @@ public class ECDSAPublicKeyEntryDecoder extends AbstractPublicKeyEntryDecoder<EC byte[] octets = KeyEntryResolver.readRLEBytes(keyData); ECPoint w; try { - w = octetStringToEcPoint(octets); + w = ECCurves.octetStringToEcPoint(octets); if (w == null) { throw new InvalidKeySpecException("No ECPoint generated for curve=" + keyCurveName + " from octets=" + BufferUtils.toHex(':', octets)); @@ -150,7 +142,7 @@ public class ECDSAPublicKeyEntryDecoder extends AbstractPublicKeyEntryDecoder<EC KeyEntryResolver.encodeString(s, keyType); // see rfc5656 section 3.1 KeyEntryResolver.encodeString(s, curveName); - ECPointCompression.UNCOMPRESSED.writeECPoint(s, curveName, key.getW()); + ECCurves.ECPointCompression.UNCOMPRESSED.writeECPoint(s, curveName, key.getW()); return keyType; } @@ -183,200 +175,4 @@ public class ECDSAPublicKeyEntryDecoder extends AbstractPublicKeyEntryDecoder<EC throw new NoSuchProviderException("ECC not supported"); } } - - public static ECPoint octetStringToEcPoint(byte... octets) { - if (NumberUtils.isEmpty(octets)) { - return null; - } - - int startIndex = findFirstNonZeroIndex(octets); - if (startIndex < 0) { - throw new IllegalArgumentException("All zeroes ECPoint N/A"); - } - - byte indicator = octets[startIndex]; - ECPointCompression compression = ECPointCompression.fromIndicatorValue(indicator); - if (compression == null) { - throw new UnsupportedOperationException("Unknown compression indicator value: 0x" + Integer.toHexString(indicator & 0xFF)); - } - - // The coordinates actually start after the compression indicator - return compression.octetStringToEcPoint(octets, startIndex + 1, octets.length - startIndex - 1); - } - - private static int findFirstNonZeroIndex(byte... octets) { - if (NumberUtils.isEmpty(octets)) { - return -1; - } - - for (int index = 0; index < octets.length; index++) { - if (octets[index] != 0) { - return index; - } - } - - return -1; // all zeroes - } - - /** - * The various {@link ECPoint} representation compression indicators - * - * @author <a href="mailto:d...@mina.apache.org">Apache MINA SSHD Project</a> - * @see <A HREF="https://www.ietf.org/rfc/rfc5480.txt">RFC-5480 - section 2.2</A> - */ - public enum ECPointCompression { - // see http://tools.ietf.org/html/draft-jivsov-ecc-compact-00 - // see http://crypto.stackexchange.com/questions/8914/ecdsa-compressed-public-key-point-back-to-uncompressed-public-key-point - VARIANT2((byte) 0x02) { - @Override - public ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len) { - byte[] xp = new byte[len]; - System.arraycopy(octets, startIndex, xp, 0, len); - BigInteger x = octetStringToInteger(xp); - - // TODO derive even Y... - throw new UnsupportedOperationException("octetStringToEcPoint(" + name() + ")(X=" + x + ") compression support N/A"); - } - }, - VARIANT3((byte) 0x03) { - @Override - public ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len) { - byte[] xp = new byte[len]; - System.arraycopy(octets, startIndex, xp, 0, len); - BigInteger x = octetStringToInteger(xp); - - // TODO derive odd Y... - throw new UnsupportedOperationException("octetStringToEcPoint(" + name() + ")(X=" + x + ") compression support N/A"); - } - }, - UNCOMPRESSED((byte) 0x04) { - @Override - public ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len) { - int numElements = len / 2; /* x, y */ - if (len != (numElements * 2)) { // make sure length is not odd - throw new IllegalArgumentException("octetStringToEcPoint(" + name() + ") " - + " invalid remainder octets representation: " - + " expected=" + (2 * numElements) + ", actual=" + len); - } - - byte[] xp = new byte[numElements]; - byte[] yp = new byte[numElements]; - System.arraycopy(octets, startIndex, xp, 0, numElements); - System.arraycopy(octets, startIndex + numElements, yp, 0, numElements); - - BigInteger x = octetStringToInteger(xp); - BigInteger y = octetStringToInteger(yp); - return new ECPoint(x, y); - } - - @Override - public void writeECPoint(OutputStream s, String curveName, ECPoint p) throws IOException { - ECCurves curve = ECCurves.fromCurveName(curveName); - if (curve == null) { - throw new StreamCorruptedException("writeECPoint(" + name() + ")[" + curveName + "] cannot determine octets count"); - } - - int numElements = curve.getNumPointOctets(); - KeyEntryResolver.encodeInt(s, 1 /* the indicator */ + 2 * numElements); - s.write(getIndicatorValue()); - writeCoordinate(s, "X", p.getAffineX(), numElements); - writeCoordinate(s, "Y", p.getAffineY(), numElements); - } - - }; - - public static final Set<ECPointCompression> VALUES = - Collections.unmodifiableSet(EnumSet.allOf(ECPointCompression.class)); - - private final byte indicatorValue; - - ECPointCompression(byte indicator) { - indicatorValue = indicator; - } - - public final byte getIndicatorValue() { - return indicatorValue; - } - - public abstract ECPoint octetStringToEcPoint(byte[] octets, int startIndex, int len); - - public byte[] ecPointToOctetString(String curveName, ECPoint p) { - try (ByteArrayOutputStream baos = new ByteArrayOutputStream((2 * 66) + Long.SIZE)) { - writeECPoint(baos, curveName, p); - return baos.toByteArray(); - } catch (IOException e) { - throw new RuntimeException("ecPointToOctetString(" + curveName + ")" - + " failed (" + e.getClass().getSimpleName() + ")" - + " to write data: " + e.getMessage(), - e); - } - } - - public void writeECPoint(OutputStream s, String curveName, ECPoint p) throws IOException { - if (s == null) { - throw new EOFException("No output stream"); - } - - throw new StreamCorruptedException("writeECPoint(" + name() + ")[" + p + "] N/A"); - } - - protected void writeCoordinate(OutputStream s, String n, BigInteger v, int numElements) throws IOException { - byte[] vp = v.toByteArray(); - int startIndex = 0; - int vLen = vp.length; - if (vLen > numElements) { - if (vp[0] == 0) { // skip artificial positive sign - startIndex++; - vLen--; - } - } - - if (vLen > numElements) { - throw new StreamCorruptedException("writeCoordinate(" + name() + ")[" + n + "]" - + " value length (" + vLen + ") exceeds max. (" + numElements + ")" - + " for " + v); - } - - if (vLen < numElements) { - byte[] tmp = new byte[numElements]; - System.arraycopy(vp, startIndex, tmp, numElements - vLen, vLen); - vp = tmp; - } - - s.write(vp, startIndex, vLen); - } - - public static ECPointCompression fromIndicatorValue(int value) { - if ((value < 0) || (value > 0xFF)) { - return null; // must be a byte value - } - - for (ECPointCompression c : VALUES) { - if (value == c.getIndicatorValue()) { - return c; - } - } - - return null; - } - - /** - * Converts the given octet string (defined by ASN.1 specifications) to a {@link BigInteger} - * As octet strings always represent positive integers, a zero-byte is prepended to - * the given array if necessary (if is MSB equal to 1), then this is converted to BigInteger - * The conversion is defined in the Section 2.3.8 - * - * @param octets - octet string bytes to be converted - * @return The {@link BigInteger} representation of the octet string - */ - public static BigInteger octetStringToInteger(byte... octets) { - if (octets == null) { - return null; - } else if (octets.length == 0) { - return BigInteger.ZERO; - } else { - return new BigInteger(1, octets); - } - } - } } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e767438a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AESPrivateKeyObfuscator.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AESPrivateKeyObfuscator.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AESPrivateKeyObfuscator.java new file mode 100644 index 0000000..b2205ed --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AESPrivateKeyObfuscator.java @@ -0,0 +1,107 @@ +/* + * 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:d...@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 LazyValuesHolder.KEY_LENGTHS; + } + + private static class LazyValuesHolder { + private static final List<Integer> KEY_LENGTHS = + Collections.unmodifiableList(detectSupportedKeySizes()); + + // 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/e767438a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractKeyPairResourceParser.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractKeyPairResourceParser.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractKeyPairResourceParser.java index 7021830..de144be 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractKeyPairResourceParser.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractKeyPairResourceParser.java @@ -26,7 +26,6 @@ import java.io.StreamCorruptedException; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.util.ArrayList; -import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; @@ -138,12 +137,7 @@ public abstract class AbstractKeyPairResourceParser extends AbstractLoggingBean public Collection<KeyPair> extractKeyPairs( String resourceKey, String beginMarker, String endMarker, FilePasswordProvider passwordProvider, List<String> lines) throws IOException, GeneralSecurityException { - String data = GenericUtils.join(lines, ' '); - data = data.replaceAll("\\s", ""); - data = data.trim(); - - Base64.Decoder decoder = Base64.getDecoder(); - return extractKeyPairs(resourceKey, beginMarker, endMarker, passwordProvider, decoder.decode(data)); + return extractKeyPairs(resourceKey, beginMarker, endMarker, passwordProvider, KeyPairResourceParser.extractDataBytes(lines)); } /** http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e767438a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractPrivateKeyObfuscator.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractPrivateKeyObfuscator.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/AbstractPrivateKeyObfuscator.java new file mode 100644 index 0000000..ac93ab4 --- /dev/null +++ b/sshd-core/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:d...@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/e767438a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/DESPrivateKeyObfuscator.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/DESPrivateKeyObfuscator.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/DESPrivateKeyObfuscator.java new file mode 100644 index 0000000..2043f06 --- /dev/null +++ b/sshd-core/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:d...@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/e767438a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceParser.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceParser.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceParser.java index beb592d..6c1f508 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceParser.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/KeyPairResourceParser.java @@ -23,6 +23,7 @@ import java.io.IOException; import java.security.GeneralSecurityException; import java.security.KeyPair; import java.util.Arrays; +import java.util.Base64; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; @@ -69,6 +70,23 @@ public interface KeyPairResourceParser extends KeyPairResourceLoader { */ 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 + */ + static byte[] extractDataBytes(Collection<String> lines) { + String data = GenericUtils.join(lines, ' '); + data = data.replaceAll("\\s", ""); + data = data.trim(); + + Base64.Decoder decoder = Base64.getDecoder(); + return decoder.decode(data); + } + static boolean containsMarkerLine(List<String> lines, String marker) { return containsMarkerLine(lines, Collections.singletonList(ValidateUtils.checkNotNullAndNotEmpty(marker, "No marker"))); } http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e767438a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyEncryptionContext.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyEncryptionContext.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyEncryptionContext.java new file mode 100644 index 0000000..d7ce237 --- /dev/null +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyEncryptionContext.java @@ -0,0 +1,276 @@ +/* + * 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.Objects; +import java.util.SortedSet; +import java.util.TreeMap; + +import org.apache.sshd.common.util.GenericUtils; +import org.apache.sshd.common.util.ValidateUtils; + +/** + * @author <a href="mailto:d...@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 = + new TreeMap<String, PrivateKeyObfuscator>(String.CASE_INSENSITIVE_ORDER) { + private static final long serialVersionUID = 1L; // no serialization expected + + { + for (PrivateKeyObfuscator o : new PrivateKeyObfuscator[]{ + AESPrivateKeyObfuscator.INSTANCE, DESPrivateKeyObfuscator.INSTANCE + }) { + put(o.getCipherName(), o); + } + } + }; + + 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 SortedSet<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 alogrithm 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/e767438a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyObfuscator.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyObfuscator.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/PrivateKeyObfuscator.java new file mode 100644 index 0000000..d8d2db5 --- /dev/null +++ b/sshd-core/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:d...@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/e767438a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHDSSPrivateKeyEntryDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHDSSPrivateKeyEntryDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHDSSPrivateKeyEntryDecoder.java index 02cae65..6188a04 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHDSSPrivateKeyEntryDecoder.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHDSSPrivateKeyEntryDecoder.java @@ -96,14 +96,7 @@ public class OpenSSHDSSPrivateKeyEntryDecoder extends AbstractPrivateKeyEntryDec @Override public DSAPublicKey recoverPublicKey(DSAPrivateKey privateKey) throws GeneralSecurityException { - // based on code from https://github.com/alexo/SAML-2.0/blob/master/java-opensaml/opensaml-security-api/src/main/java/org/opensaml/xml/security/SecurityHelper.java - DSAParams keyParams = privateKey.getParams(); - BigInteger p = keyParams.getP(); - BigInteger x = privateKey.getX(); - BigInteger q = keyParams.getQ(); - BigInteger g = keyParams.getG(); - BigInteger y = g.modPow(x, p); - return generatePublicKey(new DSAPublicKeySpec(y, p, q, g)); + return KeyUtils.recoverDSAPublicKey(privateKey); } @Override http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e767438a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java index bc41acb..07f970f 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHKeyPairResourceParser.java @@ -246,6 +246,7 @@ public class OpenSSHKeyPairResourceParser extends AbstractKeyPairResourceParser return stream; } + /** * @param decoder The decoder to register * @throws IllegalArgumentException if no decoder or not key type or no http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/e767438a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHRSAPrivateKeyDecoder.java ---------------------------------------------------------------------- diff --git a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHRSAPrivateKeyDecoder.java b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHRSAPrivateKeyDecoder.java index e957d0b..72e003f 100644 --- a/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHRSAPrivateKeyDecoder.java +++ b/sshd-core/src/main/java/org/apache/sshd/common/config/keys/loader/openssh/OpenSSHRSAPrivateKeyDecoder.java @@ -81,7 +81,6 @@ public class OpenSSHRSAPrivateKeyDecoder extends AbstractPrivateKeyEntryDecoder< return generatePrivateKey(new RSAPrivateKeySpec(n, d)); } - @Override public boolean isPublicKeyRecoverySupported() { return true; @@ -89,23 +88,7 @@ public class OpenSSHRSAPrivateKeyDecoder extends AbstractPrivateKeyEntryDecoder< @Override public RSAPublicKey recoverPublicKey(RSAPrivateKey privateKey) throws GeneralSecurityException { - if (privateKey instanceof RSAPrivateCrtKey) { - return recoverFromRSAPrivateCrtKey((RSAPrivateCrtKey) privateKey); - } else { - return recoverPublicKey(privateKey.getModulus(), privateKey.getPrivateExponent()); - } - } - - protected RSAPublicKey recoverPublicKey(BigInteger modulus, BigInteger privateExponent) throws GeneralSecurityException { - return generatePublicKey(new RSAPublicKeySpec(modulus, DEFAULT_PUBLIC_EXPONENT)); - } - - protected RSAPublicKey recoverFromRSAPrivateCrtKey(RSAPrivateCrtKey rsaKey) throws GeneralSecurityException { - BigInteger p = rsaKey.getPrimeP(); - BigInteger q = rsaKey.getPrimeQ(); - BigInteger n = p.multiply(q); - BigInteger e = rsaKey.getPublicExponent(); - return generatePublicKey(new RSAPublicKeySpec(n, e)); + return KeyUtils.recoverRSAPublicKey(privateKey); } @Override