You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@mina.apache.org by lg...@apache.org on 2016/08/02 03:13:59 UTC
mina-sshd git commit: [SSHD-685] Provide support for VisualHostKey
(a.k.a. randomArt)
Repository: mina-sshd
Updated Branches:
refs/heads/master 549ac4e2a -> 626dca723
[SSHD-685] Provide support for VisualHostKey (a.k.a. randomArt)
Project: http://git-wip-us.apache.org/repos/asf/mina-sshd/repo
Commit: http://git-wip-us.apache.org/repos/asf/mina-sshd/commit/626dca72
Tree: http://git-wip-us.apache.org/repos/asf/mina-sshd/tree/626dca72
Diff: http://git-wip-us.apache.org/repos/asf/mina-sshd/diff/626dca72
Branch: refs/heads/master
Commit: 626dca723864745f9523f95dc5c7a46483b54e0e
Parents: 549ac4e
Author: Lyor Goldstein <ly...@gmail.com>
Authored: Tue Aug 2 06:15:26 2016 +0300
Committer: Lyor Goldstein <ly...@gmail.com>
Committed: Tue Aug 2 06:15:26 2016 +0300
----------------------------------------------------------------------
.../sshd/common/config/keys/KeyRandomArt.java | 246 +++++++++++++++++++
.../common/config/keys/KeyRandomArtTest.java | 101 ++++++++
.../sshd/common/config/keys/KeyUtils.java | 60 ++++-
.../apache/sshd/common/digest/DigestUtils.java | 21 +-
4 files changed, 416 insertions(+), 12 deletions(-)
----------------------------------------------------------------------
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/626dca72/sshd-contrib/src/main/java/org/apache/sshd/common/config/keys/KeyRandomArt.java
----------------------------------------------------------------------
diff --git a/sshd-contrib/src/main/java/org/apache/sshd/common/config/keys/KeyRandomArt.java b/sshd-contrib/src/main/java/org/apache/sshd/common/config/keys/KeyRandomArt.java
new file mode 100644
index 0000000..19fc80f
--- /dev/null
+++ b/sshd-contrib/src/main/java/org/apache/sshd/common/config/keys/KeyRandomArt.java
@@ -0,0 +1,246 @@
+/*
+ * 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;
+
+import java.io.IOException;
+import java.io.StreamCorruptedException;
+import java.security.PublicKey;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Objects;
+
+import org.apache.sshd.common.Factory;
+import org.apache.sshd.common.digest.Digest;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.common.util.ValidateUtils;
+
+/**
+ * Draw an ASCII-Art representing the fingerprint so human brain can
+ * profit from its built-in pattern recognition ability.
+ * This technique is called "random art" and can be found in some
+ * scientific publications like this original paper:
+ *
+ * "Hash Visualization: a New Technique to improve Real-World Security",
+ * Perrig A. and Song D., 1999, International Workshop on Cryptographic
+ * Techniques and E-Commerce (CrypTEC '99)
+ *
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ * @see <a href="http://sparrow.ece.cmu.edu/~adrian/projects/validation/validation.pdf">Original article</a>
+ * @see <a href="http://opensource.apple.com/source/OpenSSH/OpenSSH-175/openssh/key.c">C implementation</a>
+ */
+public class KeyRandomArt {
+ public static final int FLDBASE = 8;
+ public static final int FLDSIZE_Y = FLDBASE + 1;
+ public static final int FLDSIZE_X = FLDBASE * 2 + 1;
+ public static final String AUGMENTATION_STRING = " .o+=*BOX@%&#/^SE";
+
+ private final String algorithm;
+ private final int keySize;
+ private final char[][] field = new char[FLDSIZE_X][FLDSIZE_Y];
+
+ public KeyRandomArt(PublicKey key) throws Exception {
+ this(key, KeyUtils.getDefaultFingerPrintFactory());
+ }
+
+ public KeyRandomArt(PublicKey key, Factory<? extends Digest> f) throws Exception {
+ this(key, Objects.requireNonNull(f, "No digest factory").create());
+ }
+
+ public KeyRandomArt(PublicKey key, Digest d) throws Exception {
+ this(Objects.requireNonNull(key, "No key provided").getAlgorithm(),
+ KeyUtils.getKeySize(key),
+ KeyUtils.getRawFingerprint(Objects.requireNonNull(d, "No key digest"), key));
+ }
+
+ /**
+ * @param algorithm The key algorithm
+ * @param keySize The key size in bits
+ * @param digest The key digest
+ */
+ public KeyRandomArt(String algorithm, int keySize, byte[] digest) {
+ this.algorithm = ValidateUtils.checkNotNullAndNotEmpty(algorithm, "No algorithm provided");
+ ValidateUtils.checkTrue(keySize > 0, "Invalid key size: %d", keySize);
+ this.keySize = keySize;
+ Objects.requireNonNull(digest, "No key digest provided");
+
+ int x = FLDSIZE_X / 2;
+ int y = FLDSIZE_Y / 2;
+ int len = AUGMENTATION_STRING.length() - 1;
+ for (int i = 0; i < digest.length; i++) {
+ /* each byte conveys four 2-bit move commands */
+ int input = digest[i] & 0xFF;
+ for (int b = 0; b < 4; b++) {
+ /* evaluate 2 bit, rest is shifted later */
+ x += ((input & 0x1) != 0) ? 1 : -1;
+ y += ((input & 0x2) != 0) ? 1 : -1;
+
+ /* assure we are still in bounds */
+ x = Math.max(x, 0);
+ y = Math.max(y, 0);
+ x = Math.min(x, FLDSIZE_X - 1);
+ y = Math.min(y, FLDSIZE_Y - 1);
+
+ /* augment the field */
+ if (field[x][y] < (len - 2)) {
+ field[x][y]++;
+ }
+ input = input >> 2;
+ }
+ }
+
+ /* mark starting point and end point*/
+ field[FLDSIZE_X / 2][FLDSIZE_Y / 2] = (char) (len - 1);
+ field[x][y] = (char) len;
+ }
+
+ public String getAlgorithm() {
+ return algorithm;
+ }
+
+ public int getKeySize() {
+ return keySize;
+ }
+
+ /**
+ * Outputs the generated random art
+ *
+ * @param <A> The {@link Appendable} output writer
+ * @param sb The writer
+ * @return The updated writer instance
+ * @throws IOException If failed to write the combined result
+ */
+ public <A extends Appendable> A append(A sb) throws IOException {
+ // Upper border
+ String s = String.format("+--[%4s %4d]", getAlgorithm(), getKeySize());
+ sb.append(s);
+ for (int index = s.length(); index <= FLDSIZE_X; index++) {
+ sb.append('-');
+ }
+ sb.append('+');
+ sb.append('\n');
+
+ // contents
+ int len = AUGMENTATION_STRING.length() - 1;
+ for (int y = 0; y < FLDSIZE_Y; y++) {
+ sb.append('|');
+ for (int x = 0; x < FLDSIZE_X; x++) {
+ char ch = field[x][y];
+ sb.append(AUGMENTATION_STRING.charAt(Math.min(ch, len)));
+ }
+ sb.append('|');
+ sb.append('\n');
+ }
+
+ // lower border
+ sb.append('+');
+ for (int index = 0; index < FLDSIZE_X; index++) {
+ sb.append('-');
+ }
+
+ sb.append('+');
+ sb.append('\n');
+ return sb;
+ }
+
+ @Override
+ public String toString() {
+ try {
+ return append(new StringBuilder((FLDSIZE_X + 4) * (FLDSIZE_Y + 3))).toString();
+ } catch (IOException e) {
+ return e.getClass().getSimpleName(); // unexpected
+ }
+ }
+
+ /**
+ * Combines the arts in a user-friendly way so they are aligned with each other
+ *
+ * @param separator The separator to use between the arts - if empty char
+ * ('\0') then no separation is done
+ * @param arts The {@link KeyRandomArt}s to combine - ignored if {@code null}/empty
+ * @return The combined result
+ */
+ public static String combine(char separator, Collection<? extends KeyRandomArt> arts) {
+ if (GenericUtils.isEmpty(arts)) {
+ return "";
+ }
+
+ try {
+ return combine(new StringBuilder(arts.size() * (FLDSIZE_X + 4) * (FLDSIZE_Y + 3)), separator, arts).toString();
+ } catch (IOException e) {
+ return e.getClass().getSimpleName(); // unexpected
+ }
+ }
+
+ /**
+ * Combines the arts in a user-friendly way so they are aligned with each other
+ *
+ * @param <A> The {@link Appendable} output writer
+ * @param sb The writer
+ * @param separator The separator to use between the arts - if empty char
+ * ('\0') then no separation is done
+ * @param arts The {@link KeyRandomArt}s to combine - ignored if {@code null}/empty
+ * @return The updated writer instance
+ * @throws IOException If failed to write the combined result
+ */
+ public static <A extends Appendable> A combine(A sb, char separator, Collection<? extends KeyRandomArt> arts) throws IOException {
+ if (GenericUtils.isEmpty(arts)) {
+ return sb;
+ }
+
+ List<String[]> allLines = new ArrayList<>(arts.size());
+ int numLines = -1;
+ for (KeyRandomArt a : arts) {
+ String s = a.toString();
+ String[] lines = GenericUtils.split(s, '\n');
+ if (numLines <= 0) {
+ numLines = lines.length;
+ } else {
+ if (numLines != lines.length) {
+ throw new StreamCorruptedException("Mismatched lines count: expected=" + numLines + ", actual=" + lines.length);
+ }
+ }
+
+ for (int index = 0; index < lines.length; index++) {
+ String l = lines[index];
+ if ((l.length() > 0) && (l.charAt(l.length() - 1) == '\r')) {
+ l = l.substring(0, l.length() - 1);
+ lines[index] = l;
+ }
+ }
+
+ allLines.add(lines);
+ }
+
+ for (int row = 0; row < numLines; row++) {
+ for (int index = 0; index < allLines.size(); index++) {
+ String[] lines = allLines.get(index);
+ String l = lines[row];
+ sb.append(l);
+ if ((index > 0) && (separator != '\0')) {
+ sb.append(separator);
+ }
+ }
+ sb.append('\n');
+ }
+
+ return sb;
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/626dca72/sshd-contrib/src/test/java/org/apache/sshd/common/config/keys/KeyRandomArtTest.java
----------------------------------------------------------------------
diff --git a/sshd-contrib/src/test/java/org/apache/sshd/common/config/keys/KeyRandomArtTest.java b/sshd-contrib/src/test/java/org/apache/sshd/common/config/keys/KeyRandomArtTest.java
new file mode 100644
index 0000000..edea5f1
--- /dev/null
+++ b/sshd-contrib/src/test/java/org/apache/sshd/common/config/keys/KeyRandomArtTest.java
@@ -0,0 +1,101 @@
+/*
+ * 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;
+
+import java.security.KeyPair;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.List;
+
+import org.apache.sshd.common.cipher.ECCurves;
+import org.apache.sshd.common.util.GenericUtils;
+import org.apache.sshd.util.test.BaseTestSupport;
+import org.apache.sshd.util.test.Utils;
+import org.junit.AfterClass;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+/**
+ * @author <a href="mailto:dev@mina.apache.org">Apache MINA SSHD Project</a>
+ */
+@RunWith(Parameterized.class) // see https://github.com/junit-team/junit/wiki/Parameterized-tests
+public class KeyRandomArtTest extends BaseTestSupport {
+ private static final Collection<KeyRandomArt> ARTS = new LinkedList<>();
+
+ private final String algorithm;
+ private final int keySize;
+ private final KeyPair keyPair;
+
+ public KeyRandomArtTest(String algorithm, int keySize) throws Exception {
+ this.algorithm = algorithm;
+ this.keySize = keySize;
+ this.keyPair = Utils.generateKeyPair(algorithm, keySize);
+ }
+
+ @Parameters(name = "algorithm={0}, key-size={1}")
+ public static List<Object[]> parameters() {
+ List<Object[]> params = new ArrayList<>();
+ for (int keySize : new int[]{1024, 2048, 4096}) {
+ params.add(new Object[]{KeyUtils.RSA_ALGORITHM, keySize});
+ }
+
+ for (int keySize : new int[]{512, 1024}) {
+ params.add(new Object[]{KeyUtils.DSS_ALGORITHM, keySize});
+ }
+
+ for (ECCurves curve : ECCurves.VALUES) {
+ params.add(new Object[]{KeyUtils.EC_ALGORITHM, curve.getKeySize()});
+ }
+
+ return params;
+ }
+
+ @AfterClass
+ public static void dumpAllArts() throws Exception {
+ KeyRandomArt.combine(System.out, ' ', ARTS);
+ }
+
+ @Test
+ public void testRandomArtString() throws Exception {
+ KeyRandomArt art = new KeyRandomArt(keyPair.getPublic());
+ assertEquals("Mismatched algorithem", algorithm, art.getAlgorithm());
+ assertEquals("Mismatched key size", keySize, art.getKeySize());
+
+ String s = art.toString();
+ String[] lines = GenericUtils.split(s, '\n');
+ assertEquals("Mismatched lines count", KeyRandomArt.FLDSIZE_Y + 2, lines.length);
+
+ for (int index = 0; index < lines.length; index++) {
+ String l = lines[index];
+ if ((l.length() > 0) && (l.charAt(l.length() - 1) == '\r')) {
+ l = l.substring(0, l.length() - 1);
+ lines[index] = l;
+ }
+ System.out.append('\t').println(l);
+
+ assertTrue("Mismatched line length #" + (index + 1) + ": " + l.length(), l.length() >= (KeyRandomArt.FLDSIZE_X + 2));
+ }
+
+ ARTS.add(art);
+ }
+}
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/626dca72/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 0ee6ff8..5dc90ef 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
@@ -19,6 +19,7 @@
package org.apache.sshd.common.config.keys;
import java.io.IOException;
+import java.math.BigInteger;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
@@ -117,10 +118,10 @@ public final class KeyUtils {
private static final AtomicReference<DigestFactory> DEFAULT_DIGEST_HOLDER = new AtomicReference<>();
private static final Map<String, PublicKeyEntryDecoder<?, ?>> BY_KEY_TYPE_DECODERS_MAP =
- new TreeMap<String, PublicKeyEntryDecoder<?, ?>>(String.CASE_INSENSITIVE_ORDER);
+ new TreeMap<>(String.CASE_INSENSITIVE_ORDER);
private static final Map<Class<?>, PublicKeyEntryDecoder<?, ?>> BY_KEY_CLASS_DECODERS_MAP =
- new HashMap<Class<?>, PublicKeyEntryDecoder<?, ?>>();
+ new HashMap<>();
static {
registerPublicKeyEntryDecoder(RSAPublicKeyDecoder.INSTANCE);
@@ -188,20 +189,20 @@ public final class KeyUtils {
if (perms.contains(PosixFilePermission.OTHERS_EXECUTE)) {
PosixFilePermission p = PosixFilePermission.OTHERS_EXECUTE;
- return new Pair<String, Object>(String.format("Permissions violation (%s)", p), p);
+ return new Pair<>(String.format("Permissions violation (%s)", p), p);
}
if (OsUtils.isUNIX()) {
PosixFilePermission p = IoUtils.validateExcludedPermissions(perms, STRICTLY_PROHIBITED_FILE_PERMISSION);
if (p != null) {
- return new Pair<String, Object>(String.format("Permissions violation (%s)", p), p);
+ return new Pair<>(String.format("Permissions violation (%s)", p), p);
}
if (Files.isRegularFile(path, options)) {
Path parent = path.getParent();
p = IoUtils.validateExcludedPermissions(IoUtils.getPermissions(parent, options), STRICTLY_PROHIBITED_FILE_PERMISSION);
if (p != null) {
- return new Pair<String, Object>(String.format("Parent permissions violation (%s)", p), p);
+ return new Pair<>(String.format("Parent permissions violation (%s)", p), p);
}
}
}
@@ -223,14 +224,14 @@ public final class KeyUtils {
}
if (!expected.contains(owner)) {
- return new Pair<String, Object>(String.format("Owner violation (%s)", owner), owner);
+ return new Pair<>(String.format("Owner violation (%s)", owner), owner);
}
if (OsUtils.isUNIX()) {
if (Files.isRegularFile(path, options)) {
String parentOwner = IoUtils.getFileOwner(path.getParent(), options);
if ((!GenericUtils.isEmpty(parentOwner)) && (!expected.contains(parentOwner))) {
- return new Pair<String, Object>(String.format("Parent owner violation (%s)", parentOwner), parentOwner);
+ return new Pair<>(String.format("Parent owner violation (%s)", parentOwner), parentOwner);
}
}
}
@@ -460,7 +461,7 @@ public final class KeyUtils {
* @see #getFingerPrint(Digest, PublicKey)
*/
public static String getFingerPrint(Factory<? extends Digest> f, PublicKey key) {
- return (key == null) ? null : getFingerPrint(ValidateUtils.checkNotNull(f, "No digest factory").create(), key);
+ return (key == null) ? null : getFingerPrint(Objects.requireNonNull(f, "No digest factory").create(), key);
}
/**
@@ -484,6 +485,24 @@ public final class KeyUtils {
}
}
+ public static byte[] getRawFingerprint(PublicKey key) throws Exception {
+ return getRawFingerprint(getDefaultFingerPrintFactory(), key);
+ }
+
+ public static byte[] getRawFingerprint(Factory<? extends Digest> f, PublicKey key) throws Exception {
+ return (key == null) ? null : getRawFingerprint(Objects.requireNonNull(f, "No digest factory").create(), key);
+ }
+
+ public static byte[] getRawFingerprint(Digest d, PublicKey key) throws Exception {
+ if (key == null) {
+ return null;
+ }
+
+ Buffer buffer = new ByteArrayBuffer();
+ buffer.putRawPublicKey(key);
+ return DigestUtils.getRawFingerprint(d, buffer.array(), 0, buffer.wpos());
+ }
+
/**
* @param f The {@link Factory} to create the {@link Digest} to use
* @param s The {@link String} to digest - ignored if {@code null}/empty,
@@ -656,6 +675,31 @@ public final class KeyUtils {
}
/**
+ * Determines the key size in bits
+ *
+ * @param key The {@link Key} to examine - ignored if {@code null}
+ * @return The key size - non-positive value if cannot determine it
+ */
+ public static int getKeySize(Key key) {
+ if (key instanceof RSAKey) {
+ BigInteger n = ((RSAKey) key).getModulus();
+ return n.bitLength();
+ } else if (key instanceof DSAKey) {
+ DSAParams params = ((DSAKey) key).getParams();
+ BigInteger p = params.getP();
+ return p.bitLength();
+ } else if (key instanceof ECKey) {
+ ECParameterSpec ecSpec = ((ECKey) key).getParams();
+ ECCurves curve = ECCurves.fromCurveParameters(ecSpec);
+ if (curve != null) {
+ return curve.getKeySize();
+ }
+ }
+
+ return -1;
+ }
+
+ /**
* @param key The {@link PublicKey} to be checked - ignored if {@code null}
* @param keySet The keys to be searched - ignored if {@code null}/empty
* @return The matching {@link PublicKey} from the keys or {@code null} if
http://git-wip-us.apache.org/repos/asf/mina-sshd/blob/626dca72/sshd-core/src/main/java/org/apache/sshd/common/digest/DigestUtils.java
----------------------------------------------------------------------
diff --git a/sshd-core/src/main/java/org/apache/sshd/common/digest/DigestUtils.java b/sshd-core/src/main/java/org/apache/sshd/common/digest/DigestUtils.java
index 56863e1..497f267 100644
--- a/sshd-core/src/main/java/org/apache/sshd/common/digest/DigestUtils.java
+++ b/sshd-core/src/main/java/org/apache/sshd/common/digest/DigestUtils.java
@@ -193,16 +193,14 @@ public final class DigestUtils {
* @param len The length of data - ignored if non-positive
* @return The fingerprint - {@code null} if non-positive length
* @throws Exception If failed to calculate the fingerprint
+ * @see #getRawFingerprint(Digest, byte[], int, int)
*/
public static String getFingerPrint(Digest d, byte[] buf, int offset, int len) throws Exception {
if (len <= 0) {
return null;
}
- ValidateUtils.checkNotNull(d, "No digest").init();
- d.update(buf, offset, len);
-
- byte[] data = d.digest();
+ byte[] data = getRawFingerprint(d, buf, offset, len);
String algo = d.getAlgorithm();
if (BuiltinDigests.md5.getAlgorithm().equals(algo)) {
return algo + ":" + BufferUtils.toHex(':', data).toLowerCase();
@@ -210,4 +208,19 @@ public final class DigestUtils {
return algo.replace("-", "").toUpperCase() + ":" + Base64.encodeToString(data).replaceAll("=", "");
}
}
+
+ public static byte[] getRawFingerprint(Digest d, byte... buf) throws Exception {
+ return getRawFingerprint(d, buf, 0, NumberUtils.length(buf));
+ }
+
+ public static byte[] getRawFingerprint(Digest d, byte[] buf, int offset, int len) throws Exception {
+ if (len <= 0) {
+ return null;
+ }
+
+ ValidateUtils.checkNotNull(d, "No digest").init();
+ d.update(buf, offset, len);
+
+ return d.digest();
+ }
}