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:
+ *
+ * &quot;Hash Visualization: a New Technique to improve Real-World Security&quot;,
+ * 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();
+    }
 }