KnownHostsTest.java

package com.jcraft.jsch;

import static java.nio.charset.StandardCharsets.ISO_8859_1;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.junit.jupiter.api.Assertions.fail;

import com.jcraft.jsch.KnownHosts.HashedHostKey;
import com.jcraft.jsch.jce.HMACSHA256;
import com.jcraft.jsch.jce.HMACSHA512;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.util.Arrays;
import java.util.Hashtable;
import java.util.LinkedList;
import java.util.Properties;
import java.util.function.Function;
import java.util.stream.Collectors;
import javax.crypto.Mac;
import javax.crypto.SecretKey;
import javax.crypto.ShortBufferException;
import javax.crypto.spec.SecretKeySpec;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;

class KnownHostsTest {
  private static final String rsaKey =
      "AAAAB3NzaC1yc2EAAAABIwAAAQEAq2A7hRGmdnm9tUDbO9IDSwBK6TbQa+PXYPCPy6rbTrTtw7PHkc"
          + "cKrpp0yVhp5HdEIcKr6pLlVDBfOLX9QUsyCOV0wzfjIJNlGEYsdlLJizHhbn2mUjvSAHQqZETYP81e"
          + "FzLQNnPHt4EVVUh7VfDESU84KezmD5QlWpXLmvU31/yMf+Se8xhHTvKSCZIFImWwoG6mbUoWf9nzpI"
          + "oaSjB+weqqUUmpaaasXVal72J+UX2B+2RPW3RcT0eOzQgqlJL3RKrTJvdsjE3JEAvGq3lGHSZXy28G"
          + "3skua2SmVi/w4yCE6gbODqnTWlg7+wC604ydGXA8VJiS5ap43JXiUFFAaQ==";
  private static final String hashValue =
      "|1|F1E1KeoE/eEWhi10WpGv4OdiO6Y=|3988QV0VE8wmZL7suNrYQLITLCg=";
  private static final String hostLine = "ssh.example.com,192.168.1.61";
  private static final byte[] dsaKeyBytes = Util.str2byte("    ssh-dsa");
  private static final byte[] rsaKeyBytes = Util.str2byte("    ssh-rsa");
  private LinkedList<String> messages;
  private JSch jsch;
  private Hashtable<String, String> orgConfig;
  private Properties orgProps;

  @BeforeEach
  void setupTest() {
    orgProps = System.getProperties();
    Properties myProps = new Properties(orgProps);
    System.setProperties(myProps);
    orgConfig = new Hashtable<>(JSch.config);
    messages = new LinkedList<>();
    jsch = new JSch();
    jsch.setInstanceLogger(new TestLogger(messages));
  }

  @AfterEach
  void tearDownTest() {
    System.setProperties(orgProps);
    JSch.setConfig(orgConfig);
    Session.random = null;
  }

  @Test
  void testInstantiationValues() throws Exception {
    KnownHosts kh = new KnownHosts(jsch);

    assertNull(kh.getKnownHostsFile(), "check known_hosts filename");
    assertNull(kh.getKnownHostsRepositoryID(), "check repository id");
    assertNotNull(kh.hmacsha1, "hmac instance not expected to be null");
    assertSame(kh.hmacsha1, kh.getHMACSHA1(),
        "same instance should be returned with call of getHMACSHA1");
    assertSame(kh.getHMACSHA1(), kh.getHMACSHA1(),
        "same instance should be returned with call of getHMACSHA1");

    KnownHosts kh2 = new KnownHosts(jsch);
    assertNotNull(kh2.hmacsha1, "hmac instance not expected to be null");
    assertNotSame(kh.hmacsha1, kh2.hmacsha1,
        "hmac instances should be different in different KH-instances");
  }

  @Test
  void testSetKnownHostsByFilename() throws Exception {
    KnownHosts kh = new KnownHosts(jsch) {
      @Override
      void setKnownHosts(InputStream input) throws JSchException {
        messages.add("set known hosts by stream");
        StringBuilder sb = new StringBuilder();
        int read;
        try {
          while ((read = input.read()) != -1) {
            sb.append((char) read);
          }
          messages.add(sb.toString());
        } catch (IOException ioe) {
          throw new JSchException("error while reading hosts file", ioe);
        }
      }
    };

    File hostFile = File.createTempFile("setknownhosts", ".txt");
    try {
      try (FileOutputStream fos = new FileOutputStream(hostFile)) {
        fos.write("some host data".getBytes(ISO_8859_1));
      }
      System.setProperty("user.home", hostFile.getParentFile().getAbsolutePath());
      kh.setKnownHosts("some_filename:that can't exist");
      assertEquals("some_filename:that can't exist", kh.getKnownHostsFile(),
          "check known_hosts filename");
      assertEquals("some_filename:that can't exist", kh.getKnownHostsRepositoryID(),
          "check repository id");

      assertEquals(0, messages.size(), "expected no messages till now");

      kh.setKnownHosts(hostFile.getAbsolutePath());
      assertEquals(hostFile.getAbsolutePath(), kh.getKnownHostsFile(),
          "check known_hosts filename");
      assertEquals(hostFile.getAbsolutePath(), kh.getKnownHostsRepositoryID(),
          "check repository id");
      assertEquals(2, messages.size(), "expected no messages after setting");
      assertEquals("set known hosts by stream", messages.removeFirst(), "check message");
      assertEquals("some host data", messages.removeFirst(), "check message");

      File userDirFile = new File("~", hostFile.getName());
      assertEquals(hostFile.getAbsolutePath(), Util.checkTilde(userDirFile.getPath()),
          "check result of userTilde");
      kh.setKnownHosts(userDirFile.getPath());
      assertEquals(userDirFile.getPath(), kh.getKnownHostsFile(), "check known_hosts filename");
      assertEquals(userDirFile.getPath(), kh.getKnownHostsRepositoryID(), "check repository id");
      assertEquals(2, messages.size(), "expected 2 messages after setting");
      assertEquals("set known hosts by stream", messages.removeFirst(), "check message");
      assertEquals("some host data", messages.removeFirst(), "check message");
    } finally {
      hostFile.delete();
    }
  }

  @Test
  void testCreateAndGetHMACSHA1() throws Exception {
    JSch.setConfig("hmac-sha1", "my.hmac.sha1.class.Name");
    KnownHosts kh = new KnownHosts(jsch) {
      @Override
      MAC createHMAC(String hmacClassname) {
        messages.add("create hmac instance of class " + hmacClassname);
        return null;
      }
    };

    assertNull(kh.hmacsha1, "hmac instance should be null");
    assertEquals("create hmac instance of class my.hmac.sha1.class.Name",
        messages.stream().collect(Collectors.joining("\r\n")));
    messages.clear();

    try {
      new KnownHosts(jsch);
      fail("exception expected");
    } catch (IllegalArgumentException iae) {
      assertEquals("instantiation of my.hmac.sha1.class.Name lead to an error", iae.getMessage(),
          "check exception message");
      Throwable cause = iae.getCause();
      assertNotNull(cause, "cause should not be null");
      assertEquals(ClassNotFoundException.class.getName(), cause.getClass().getName(),
          "unexpected cause");
    }
    assertEquals(
        "M(3): unable to instantiate HMAC-class my.hmac.sha1.class.Name\r\n"
            + "  java.lang.ClassNotFoundException: my.hmac.sha1.class.Name",
        messages.stream().collect(Collectors.joining("\r\n")));
    messages.clear();

    // it's not SHA-1 but for this test any hashing class will do
    JSch.setConfig("hmac-sha1", HMACSHA256.class.getName());
    kh = new KnownHosts(jsch);
    assertNotNull(kh.hmacsha1, "hmac instance should not be null");
    assertSame(kh.hmacsha1, kh.getHMACSHA1(), "instance shouldn't change");
    assertEquals(HMACSHA256.class.getName(), kh.hmacsha1.getClass().getName(),
        "hmac class mismatch");

    MAC currentMAC = kh.hmacsha1;
    JSch.setConfig("hmac-sha1", HMACSHA512.class.getName());
    assertSame(currentMAC, kh.getHMACSHA1(),
        "instance shouldn't change even with now correct config-entry");

    kh = new KnownHosts(jsch);
    assertNotNull(kh.hmacsha1, "hmac instance should not be null");
    assertSame(kh.hmacsha1, kh.getHMACSHA1(), "instance shouldn't change");
    assertEquals(HMACSHA512.class.getName(), kh.hmacsha1.getClass().getName(),
        "hmac class mismatch");
    assertEquals("", messages.stream().collect(Collectors.joining("\r\n")));
  }

  @Test
  void testSetKnownHostsHashedHost() throws Exception {
    KnownHosts kh = new KnownHosts(jsch);
    // comment with umlaut to check used charset for dump
    kh.setKnownHosts(
        new ByteArrayInputStream((hashValue + " " + "ssh-rsa " + rsaKey + " some comment\r\n"
            + "# 192.168.1.61 ssh-rsa MYRSAKEY some other comm��nt").getBytes(UTF_8)));

    assertEquals(0, messages.size(), "no messages expected");
    HostKey[] keys = kh.getHostKey();
    checkResultForKeyResult(keys, rsaKey, hashValue, "");

    keys = kh.getHostKey("192.168.1.61", "ssh-rsa");
    checkResultForKeyResult(keys, rsaKey, hashValue, "");

    keys = kh.getHostKey("192.168.1.62", "ssh-rsa");
    assertNotNull(keys, "actual keys expected");
    assertEquals(0, keys.length, "0 keys expected");
    keys = kh.getHostKey("192.168.1.61", "ssh-dsa");
    assertNotNull(keys, "actual keys expected");
    assertEquals(0, keys.length, "0 keys expected");

    ByteArrayOutputStream dump = new ByteArrayOutputStream();
    kh.dump(dump);
    assertEquals(
        hashValue + " ssh-rsa " + rsaKey + " some comment\n"
            + "# 192.168.1.61 ssh-rsa MYRSAKEY some other comm��nt\n" + "",
        dump.toString("UTF8"), "dump mismatch");
  }

  @Test
  void testSetKnownHostsDirectHost() throws Exception {
    KnownHosts kh = new KnownHosts(jsch);
    kh.setKnownHosts(new ByteArrayInputStream(
        ("@cert-authority " + hostLine + " " + "ssh-rsa " + rsaKey + " some comment")
            .getBytes(ISO_8859_1)));

    assertEquals(0, messages.size(), "no messages expected");
    HostKey[] keys = kh.getHostKey();
    checkResultForKeyResult(keys, rsaKey, hostLine, "@cert-authority");

    keys = kh.getHostKey("192.168.1.61", "ssh-rsa");
    checkResultForKeyResult(keys, rsaKey, hostLine, "@cert-authority");
    keys = kh.getHostKey("ssh.example.com", "ssh-rsa");
    checkResultForKeyResult(keys, rsaKey, hostLine, "@cert-authority");

    keys = kh.getHostKey("192.168.1.62", "ssh-rsa");
    assertNotNull(keys, "actual keys expected");
    assertEquals(0, keys.length, "0 keys expected");
    keys = kh.getHostKey("192.168.1.61", "ssh-dsa");
    assertNotNull(keys, "actual keys expected");
    assertEquals(0, keys.length, "0 keys expected");

    ByteArrayOutputStream dump = new ByteArrayOutputStream();
    kh.dump(dump);
    assertEquals("@cert-authority " + hostLine + " ssh-rsa " + rsaKey + " some comment\n" + "",
        dump.toString("8859_1"), "dump mismatch");

    kh.setKnownHosts(new ByteArrayInputStream(
        ("!ssh.example.com,!192.168.1.61 " + "ssh-rsa " + rsaKey + " some comment")
            .getBytes(ISO_8859_1)));
    assertEquals(0, messages.size(), "no messages expected");
    keys = kh.getHostKey();
    checkResultForKeyResult(keys, rsaKey, "!ssh.example.com,!192.168.1.61", "");

    keys = kh.getHostKey("192.168.1.61", "ssh-rsa");
    assertNotNull(keys, "actual keys expected");
    assertEquals(0, keys.length, "0 keys expected");
    keys = kh.getHostKey("ssh.example.com", "ssh-rsa");
    assertNotNull(keys, "actual keys expected");
    assertEquals(0, keys.length, "0 keys expected");

    keys = kh.getHostKey("192.168.1.62", "ssh-rsa");
    assertNotNull(keys, "actual keys expected");
    assertEquals(0, keys.length, "0 keys expected");
    keys = kh.getHostKey("192.168.1.61", "ssh-dsa");
    assertNotNull(keys, "actual keys expected");
    assertEquals(0, keys.length, "0 keys expected");

    dump.reset();
    kh.dump(dump);
    assertEquals("!ssh.example.com,!192.168.1.61" + " ssh-rsa " + rsaKey + " some comment\n" + "",
        dump.toString("8859_1"), "dump mismatch");
  }

  @Test
  void testkSyncDump() throws Exception {
    KnownHosts kh = new KnownHosts(jsch) {
      @Override
      synchronized void sync(String foo) throws IOException {
        messages.add("sync with file '" + foo + "'");
        super.sync(foo);
      }
    };
    File tempFile = File.createTempFile("checksyncdump", ".txt");
    try {
      System.setProperty("user.home", tempFile.getParentFile().getAbsolutePath());
      assertNull(kh.getKnownHostsFile(), "known_hosts expected to be null");
      kh.sync();
      assertEquals(0, messages.size(), "no messages expected");
      kh.setKnownHosts(tempFile.getAbsolutePath());
      assertEquals(0, messages.size(), "no messages expected");
      kh.sync();
      assertEquals("sync with file '" + tempFile.getAbsolutePath() + "'",
          messages.stream().collect(Collectors.joining("\r\n")));
      messages.clear();

      kh = new KnownHosts(jsch) {
        @Override
        void dump(OutputStream out) {
          messages.add("stream based dump called with stream: " + out.getClass().getName());
          try {
            out.write("some dump data".getBytes(ISO_8859_1));
          } catch (IOException ioe) {
            Assertions.fail("exception occurred while trying to write dump to tream", ioe);
          }
        }
      };
      kh.sync(null);
      assertEquals(0, messages.size(), "no messages expected");
      kh.sync(tempFile.getAbsolutePath());
      assertEquals("stream based dump called with stream: java.io.FileOutputStream",
          messages.stream().collect(Collectors.joining("\r\n")));
      messages.clear();
      assertEquals("some dump data", getContent(tempFile.getAbsolutePath()));
      assertTrue(tempFile.delete(), "unable to delete '" + tempFile.getAbsolutePath() + "'");

      String userPath = new File("~", tempFile.getName()).getPath();
      kh.sync(userPath);
      assertEquals("stream based dump called with stream: java.io.FileOutputStream",
          messages.stream().collect(Collectors.joining("\r\n")));
      messages.clear();
      assertEquals("some dump data", getContent(tempFile.getAbsolutePath()));
      assertTrue(tempFile.delete(), "unable to delete '" + tempFile.getAbsolutePath() + "'");

      assertTrue(tempFile.mkdir(), "unable to create '" + tempFile.getAbsolutePath() + "'");
      try {
        kh.sync(tempFile.getAbsolutePath());
        fail("exception expected");
      } catch (FileNotFoundException fnfe) {
        // expected, details are OS-dependent, so no check of message, etc.
      }
      assertEquals(0, messages.size(), "no messages expected");

      kh = new KnownHosts(jsch) {
        @Override
        void dumpHostKey(OutputStream out, HostKey hk) throws IOException {
          if (out == null) {
            throw new NullPointerException("out is null");
          }
          messages.add("dump host key for host " + hk.getHost());
        }
      };

      kh.add(kh.new HashedHostKey("host1.example.com", HostKey.SSHRSA, new byte[1]), null);
      kh.add(kh.new HashedHostKey("host2.example.com", HostKey.SSHRSA, new byte[1]), null);

      kh.dump(null);
      assertEquals(
          "M(3): unable to dump known hosts\r\n" + "  java.lang.NullPointerException: out is null",
          messages.stream().collect(Collectors.joining("\r\n")));
      messages.clear();

      kh.dump(new ByteArrayOutputStream());
      assertEquals(
          "dump host key for host host1.example.com\r\n"
              + "dump host key for host host2.example.com",
          messages.stream().collect(Collectors.joining("\r\n")));
      messages.clear();
    } finally {
      tempFile.delete();
    }
  }

  @Test
  void testDumpHostKey() throws Exception {
    ByteArrayOutputStream sink = new ByteArrayOutputStream();
    KnownHosts kh = new KnownHosts(jsch);

    kh.dumpHostKey(sink,
        new HostKey("", "hostwithoutmarker", HostKey.SSHRSA, "rsakey".getBytes(ISO_8859_1), null));
    assertEquals("hostwithoutmarker ssh-rsa cnNha2V5\n", sink.toString("utf8"), "check dumped key");
    sink.reset();
    kh.dumpHostKey(sink, new HostKey("@somemarker", "hostwithmarker", HostKey.SSHRSA,
        "rsakey".getBytes(ISO_8859_1), null));
    assertEquals("@somemarker hostwithmarker ssh-rsa cnNha2V5\n", sink.toString("utf8"),
        "check dumped key");
    sink.reset();

    kh.dumpHostKey(sink, new HostKey("", "hostwithoutmarker", HostKey.SSHRSA,
        "rsakey".getBytes(ISO_8859_1), "some comm��nt"));
    assertEquals("hostwithoutmarker ssh-rsa cnNha2V5 some comm��nt\n", sink.toString("utf8"),
        "check dumped key");
    sink.reset();
    kh.dumpHostKey(sink, new HostKey("@somemarker", "hostwithmarker", HostKey.SSHRSA,
        "rsakey".getBytes(ISO_8859_1), "some comm��nt"));
    assertEquals("@somemarker hostwithmarker ssh-rsa cnNha2V5 some comm��nt\n",
        sink.toString("utf8"), "check dumped key");
    sink.reset();

    kh.dumpHostKey(sink, new HostKey("", "hostwithoutmarker", HostKey.UNKNOWN,
        "rsakey".getBytes(ISO_8859_1), "some comm��nt"));
    assertEquals("hostwithoutmarker\n", sink.toString("utf8"), "check dumped key");
    sink.reset();
  }

  @Test
  void testDeleteSubstring() {
    KnownHosts kh = new KnownHosts(jsch);
    assertEquals("host1,host2", kh.deleteSubString("todelete,host1,host2", "todelete"),
        "check result");
    assertEquals("host1,host2", kh.deleteSubString("host1,todelete,host2", "todelete"),
        "check result");
    assertEquals("host1,host2,todelete",
        kh.deleteSubString("host1,todelete,host2,todelete", "todelete"), "check result");
    assertEquals("host1,host2", kh.deleteSubString("host1,host2,todelete", "todelete"),
        "check result");
    assertEquals("host1,host2,host3", kh.deleteSubString("host1,host2,host3", "todelete"),
        "check result");
    assertEquals("host1,host2,host3,", kh.deleteSubString("host1,host2,host3,", "todelete"),
        "check result");
    assertEquals("", kh.deleteSubString("todelete", "todelete"), "check result");
    assertEquals("nottodelete", kh.deleteSubString("nottodelete", "todelete"), "check result");
  }

  @Test
  void testCreateHashedKey() throws Exception {
    Session.random = new NotSoRandomRandom();
    KnownHosts kh = new KnownHosts(jsch);
    kh.hmacsha1 = null; // this will lead to an NPE if the creation uses this instance

    try {
      kh.createHashedHostKey("host.example.com", "    ssh-rsa".getBytes(ISO_8859_1));
      fail("exception expected");
    } catch (NullPointerException npe) {
      // expected but messages differ between java versions, so we don't check the message
      assertEquals("hash", npe.getStackTrace()[0].getMethodName(), "check hash threw exception");
    }

    kh.hmacsha1 = new HMACSHA256();
    HostKey hostKey =
        kh.createHashedHostKey("host.example.com", "    ssh-rsa".getBytes(ISO_8859_1));
    assertNotNull(hostKey, "returned host key shouldn't be null");
    assertEquals(HashedHostKey.class.getName(), hostKey.getClass().getName(),
        "check type of returned host key");
    HashedHostKey hhk = (HashedHostKey) hostKey;

    assertEquals(
        "|1|AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8=|mie6rcAf1aPGk6d+HxnkpvO4HaOAH/Y6YWegs+Xog/s=",
        hhk.getHost(), "host mismatch");
    assertEquals("", hhk.getMarker(), "marker mismatch");
    assertEquals("SHA256:nPt/mXkBbUZohzkVT/XMnXF6i1pKwcdLnCClkcJq/1o", hhk.getFingerPrint(jsch),
        "fingerprint mismatch");
    assertEquals(null, hhk.getComment(), "comment mismatch");
    assertEquals("ICAgIHNzaC1yc2E=", hhk.getKey(), "key mismatch");
    assertEquals("ssh-rsa", hhk.getType(), "type mismatch");
  }

  @Test
  void testHashedKeyCreation() throws Exception {
    Session.random = new NotSoRandomRandom();
    KnownHosts kh = new KnownHosts(jsch);
    HashedHostKey hhk;

    hhk = kh.new HashedHostKey("host.example.com", dsaKeyBytes);
    checkUnhashedHostKey(hhk, "", "host.example.com", "ssh-dss", null);

    hhk.hash();
    checkSHA1HashResult(hhk, "", "ssh-dss", null);

    hhk.hash();
    checkSHA1HashResult(hhk, "", "ssh-dss", null);

    hhk = kh.new HashedHostKey("|1|AAECAwQFBgcICQoLDA0ODxAREhM=|/pE4peaossRYDRp6bEWa348eFLI=",
        dsaKeyBytes);
    checkSHA1HashResult(hhk, "", "ssh-dss", null);

    hhk = kh.new HashedHostKey("|1|AAECAwQFBgcICQoLDA0ODxAREhM=/pE4peaossRYDRp6bEWa348eFLI=",
        dsaKeyBytes);
    checkUnhashedHostKey(hhk, "", "|1|AAECAwQFBgcICQoLDA0ODxAREhM=/pE4peaossRYDRp6bEWa348eFLI=",
        "ssh-dss", null);

    hhk = kh.new HashedHostKey("|1|AAAA|ABCD", dsaKeyBytes);
    checkUnhashedHostKey(hhk, "", "|1|AAAA|ABCD", "ssh-dss", null);

    hhk =
        kh.new HashedHostKey("|1|AAECAwQFBgcICQoLDA0ODxAREhM=|ABCD", HostKey.ED25519, dsaKeyBytes);
    checkUnhashedHostKey(hhk, "", "|1|AAECAwQFBgcICQoLDA0ODxAREhM=|ABCD", "ssh-ed25519", null);

    Mac mac = Mac.getInstance("HMACSHA1", new BouncyCastleProvider());
    kh.hmacsha1 = new BCHMACSHA1(mac);
    hhk = kh.new HashedHostKey("@somemarker", "host.example.com", HostKey.ED448, dsaKeyBytes,
        "some comm��nt");
    checkUnhashedHostKey(hhk, "@somemarker", "host.example.com", "ssh-ed448", "some comm��nt");

    hhk.hash();
    checkSHA1HashResult(hhk, "@somemarker", "ssh-ed448", "some comm��nt");

    hhk.hash(); // should have no effect
    checkSHA1HashResult(hhk, "@somemarker", "ssh-ed448", "some comm��nt");

    hhk = kh.new HashedHostKey(hhk.getHost(), HostKey.ED448, dsaKeyBytes);
    checkSHA1HashResult(hhk, "", "ssh-ed448", null);
  }

  @Test
  void testHashedHostKeyHashIsMatch() throws Exception {
    String heyKey =
        "0x00:0x01:0x02:0x03:0x04:0x05:0x06:0x07:0x08:0x09:0x0a:0x0b:0x0c:0x0d:0x0e:0x0f:0x10:0x11:0x12:0x13";
    Session.random = new NotSoRandomRandom() {
      @Override
      public void fill(byte[] foo, int start, int len) {
        messages.add("fill in random called");
        super.fill(foo, start, len);
      }
    };
    KnownHosts kh = new KnownHosts(jsch);
    boolean[] throwException = new boolean[1];
    Mac mac = Mac.getInstance("HMACSHA1", new BouncyCastleProvider());
    kh.hmacsha1 = new BCHMACSHA1(mac) {
      @Override
      public void init(byte[] key) throws Exception {
        messages.add("init in mac called with key " + Util.toHex(key));
        if (throwException[0]) {
          throw new IOException("dummy ioe");
        }
        super.init(key);
      }
    };
    HashedHostKey hhk = kh.new HashedHostKey("@somemarker", "host.example.com", HostKey.ED448,
        dsaKeyBytes, "some comm��nt");
    checkUnhashedHostKey(hhk, "@somemarker", "host.example.com", "ssh-ed448", "some comm��nt");
    assertEquals(0, messages.size(), "expected no messages");
    assertTrue(hhk.isMatched("host.example.com"), "match expected");
    assertFalse(hhk.isMatched("otherhost.example.com"), "no match expected");

    hhk.hash();
    checkSHA1HashResult(hhk, "@somemarker", "ssh-ed448", "some comm��nt");
    assertEquals("fill in random called\r\n" + "init in mac called with key " + heyKey,
        messages.stream().collect(Collectors.joining("\r\n")));
    messages.clear();
    assertTrue(hhk.isMatched("host.example.com"), "match expected");
    assertEquals("init in mac called with key " + heyKey,
        messages.stream().collect(Collectors.joining("\r\n")));
    messages.clear();
    assertFalse(hhk.isMatched("otherhost.example.com"), "no match expected");
    assertEquals("init in mac called with key " + heyKey,
        messages.stream().collect(Collectors.joining("\r\n")));
    messages.clear();

    hhk.hash();
    assertEquals(0, messages.size(), "expected no messages");

    hhk = kh.new HashedHostKey("@somemarker", "host.example.com", HostKey.ED448, dsaKeyBytes,
        "some comm��nt");
    hhk.salt = new byte[mac.getMacLength()];
    Session.random.fill(hhk.salt, 0, hhk.salt.length);
    messages.clear();
    hhk.hash();
    checkSHA1HashResult(hhk, "@somemarker", "ssh-ed448", "some comm��nt");
    assertEquals("init in mac called with key " + heyKey,
        messages.stream().collect(Collectors.joining("\r\n")));
    messages.clear();

    hhk = kh.new HashedHostKey("@somemarker", "host.example.com", HostKey.ED448, dsaKeyBytes,
        "some comm��nt");
    throwException[0] = true;
    checkUnhashedHostKey(hhk, "@somemarker", "host.example.com", "ssh-ed448", "some comm��nt");
    hhk.hash();
    checkUnhashedHostKey(hhk, "@somemarker", "host.example.com", "ssh-ed448", "some comm��nt");
    assertEquals("fill in random called\r\n" + "init in mac called with key " + heyKey + "\r\n"
        + "M(3): an error occurred while trying to calculate the hash for host host.example.com\r\n"
        + "  java.io.IOException: dummy ioe",
        messages.stream().collect(Collectors.joining("\r\n")));
    messages.clear();

    throwException[0] = false;
    hhk.hash();
    checkSHA1HashResult(hhk, "@somemarker", "ssh-ed448", "some comm��nt");
    messages.clear();
    throwException[0] = true;
    assertFalse(hhk.isMatched("host.example.com"), "no match expected");
    assertEquals(
        "init in mac called with key " + heyKey + "\r\n"
            + "M(3): an error occurred while trying to check hash for host host.example.com\r\n"
            + "  java.io.IOException: dummy ioe",
        messages.stream().collect(Collectors.joining("\r\n")));
    messages.clear();
  }

  @Test
  void testSyncKnownHostsFile() throws Exception {
    boolean[] throwException = new boolean[1];
    KnownHosts kh = new KnownHosts(jsch) {
      @Override
      void sync() throws IOException {
        messages.add("sync() called");
        throw new IOException("shouldn't be called");
      }

      @Override
      synchronized void sync(String filename) throws IOException {
        messages.add("sync called with file " + filename);
        if (throwException[0]) {
          throw new RuntimeException("dummy re");
        }
      }
    };
    LinkedList<Function<String, Boolean>> promptHandlers = new LinkedList<>();
    UserInfo checkUI = new UserInfo() {
      @Override
      public void showMessage(String message) {
        messages.add("UIM: " + message);
      }

      @Override
      public boolean promptYesNo(String message) {
        messages.add("UIPYN: " + message);
        if (promptHandlers.isEmpty()) {
          return false;
        }
        Function<String, Boolean> function = promptHandlers.removeFirst();
        return function.apply(message).booleanValue();
      }

      @Override
      public boolean promptPassword(String message) {
        throw new RuntimeException("promptPassword shouldn't be called");
      }

      @Override
      public boolean promptPassphrase(String message) {
        throw new RuntimeException("promptPassphrase shouldn't be called");
      }

      @Override
      public String getPassword() {
        throw new RuntimeException("getPassword shouldn't be called");
      }

      @Override
      public String getPassphrase() {
        throw new RuntimeException("getPassphrase shouldn't be called");
      }
    };

    File tempFile = File.createTempFile("syncknownhostfile", ".dir");
    assertTrue(tempFile.delete(), "unable to delete " + tempFile.getAbsolutePath());
    File subDirFile = new File(tempFile, "subdir/known_hosts");
    String subdirParentAbsPath = subDirFile.getParentFile().getAbsolutePath();

    try {
      assertNull(kh.getKnownHostsRepositoryID(), "repository id should be null");
      kh.syncKnownHostsFile(null);
      assertEquals(0, messages.size(), "no messages expected");

      kh.setKnownHosts(tempFile.getAbsolutePath());
      assertEquals(tempFile.getAbsolutePath(), kh.getKnownHostsRepositoryID(),
          "repository id should be the temp file's absolute path");
      kh.syncKnownHostsFile(null);
      assertEquals(0, messages.size(), "no messages expected");

      promptHandlers.add((message) -> checkMessageAndReturn(
          tempFile.getAbsolutePath() + " does not exist.\n" + "Are you sure you want to create it?",
          message, false));
      kh.syncKnownHostsFile(checkUI);
      assertEquals(
          "UIPYN: " + tempFile.getAbsolutePath() + " does not exist.\n"
              + "Are you sure you want to create it?",
          messages.stream().collect(Collectors.joining("\r\n")));
      messages.clear();
      assertFalse(tempFile.exists(), "file shouldn't exist after call");

      kh.setKnownHosts(subDirFile.getAbsolutePath());
      promptHandlers.add((message) -> checkMessageAndReturn(subDirFile.getAbsolutePath()
          + " does not exist.\n" + "Are you sure you want to create it?", message, true));
      promptHandlers
          .add((message) -> checkMessageAndReturn("The parent directory " + subdirParentAbsPath
              + " does not exist.\n" + "Are you sure you want to create it?", message, false));
      kh.syncKnownHostsFile(checkUI);
      assertEquals(
          "UIPYN: " + subDirFile.getAbsolutePath() + " does not exist.\n"
              + "Are you sure you want to create it?\r\n" + "UIPYN: The parent directory "
              + subdirParentAbsPath + " does not exist.\n" + "Are you sure you want to create it?",
          messages.stream().collect(Collectors.joining("\r\n")));
      messages.clear();
      assertFalse(subDirFile.exists(), "file shouldn't exist after call");
      assertFalse(tempFile.exists(), "subdir shouldn't exist after call");

      assertTrue(tempFile.createNewFile(), "unable to create " + tempFile.getAbsolutePath());
      promptHandlers.add((message) -> checkMessageAndReturn(subDirFile.getAbsolutePath()
          + " does not exist.\n" + "Are you sure you want to create it?", message, true));
      promptHandlers
          .add((message) -> checkMessageAndReturn("The parent directory " + subdirParentAbsPath
              + " does not exist.\n" + "Are you sure you want to create it?", message, true));
      kh.syncKnownHostsFile(checkUI);
      assertEquals("UIPYN: " + subDirFile.getAbsolutePath() + " does not exist.\n"
          + "Are you sure you want to create it?\r\n" + "UIPYN: The parent directory "
          + subdirParentAbsPath + " does not exist.\n" + "Are you sure you want to create it?\r\n"
          + "UIM: " + subdirParentAbsPath + " has not been created.",
          messages.stream().collect(Collectors.joining("\r\n")));
      messages.clear();
      assertFalse(subDirFile.exists(), "file shouldn't exist after call");
      assertTrue(tempFile.isFile(), "subdir should exist as file after call");

      assertTrue(tempFile.delete(), "unable to delete " + tempFile.getAbsolutePath());
      promptHandlers.add((message) -> checkMessageAndReturn(subDirFile.getAbsolutePath()
          + " does not exist.\n" + "Are you sure you want to create it?", message, true));
      promptHandlers
          .add((message) -> checkMessageAndReturn("The parent directory " + subdirParentAbsPath
              + " does not exist.\n" + "Are you sure you want to create it?", message, true));
      kh.syncKnownHostsFile(checkUI);
      assertEquals(
          "UIPYN: " + subDirFile.getAbsolutePath() + " does not exist.\n"
              + "Are you sure you want to create it?\r\n" + "UIPYN: The parent directory "
              + subdirParentAbsPath + " does not exist.\n"
              + "Are you sure you want to create it?\r\n" + "UIM: " + subdirParentAbsPath
              + " has been succesfully created.\n" + "Please check its access permission.\r\n"
              + "sync called with file " + subDirFile.getAbsolutePath(),
          messages.stream().collect(Collectors.joining("\r\n")));
      messages.clear();
      assertFalse(subDirFile.exists(), "file shouldn't exist after call");
      assertTrue(tempFile.isDirectory(), "subdir should exist as directory after call");

      assertTrue(subDirFile.createNewFile(), "unable to create " + subDirFile.getAbsolutePath());
      kh.syncKnownHostsFile(checkUI);
      assertEquals("sync called with file " + subDirFile.getAbsolutePath(),
          messages.stream().collect(Collectors.joining("\r\n")));
      messages.clear();
      assertTrue(subDirFile.exists(), "file should exist (we've created it)");
      assertTrue(tempFile.isDirectory(), "subdir should exist as directory after call");

      throwException[0] = true;
      kh.syncKnownHostsFile(checkUI);
      assertEquals(
          "sync called with file " + subDirFile.getAbsolutePath() + "\r\n"
              + "M(3): unable to sync known host file " + subDirFile.getAbsolutePath() + "\r\n"
              + "  java.lang.RuntimeException: dummy re",
          messages.stream().collect(Collectors.joining("\r\n")));
      messages.clear();
    } finally {
      if (tempFile != null) {
        subDirFile.delete();
        subDirFile.getParentFile().delete();
        tempFile.delete();
      }
    }
  }

  @Test
  public void testCheck() throws Exception {
    KnownHosts kh = new KnownHosts(jsch);
    String expectedExceptionMessage = "";
    try {
      new HostKey("host.example.com", HostKey.GUESS, new byte[0]);
      fail("exception expected");
    } catch (Exception e) {
      expectedExceptionMessage = e.getMessage();
    }

    assertEquals(KnownHosts.NOT_INCLUDED, kh.check(null, new byte[0]),
        "null host should return NOT_INCLUDED");
    assertEquals(0, messages.size(),
        "no messages expected: " + messages.stream().collect(Collectors.joining("\r\n")));
    assertEquals(KnownHosts.NOT_INCLUDED, kh.check("host.example.com", new byte[0]),
        "empty key should return NOT_INCLUDED");
    assertEquals(1, messages.size(),
        "only one message: " + messages.stream().collect(Collectors.joining("\r\n")));
    assertEquals(
        "M(0): exception while trying to read key while checking host 'host.example.com'\r\n"
            + "  java.lang.ArrayIndexOutOfBoundsException: " + expectedExceptionMessage,
        messages.removeFirst(), "unexpected message");

    addHosts(kh);
    assertEquals(KnownHosts.NOT_INCLUDED, kh.check("host.example.com", dsaKeyBytes),
        "type mismatch should return NOT_INCLUDED");
    assertEquals(KnownHosts.OK, kh.check("host.example.com", rsaKeyBytes),
        "fitting key should return OK");
    assertEquals(KnownHosts.OK, kh.check("192.277.325.3", rsaKeyBytes),
        "fitting key should return OK");
    assertEquals(KnownHosts.OK, kh.check("192.277.325.5", dsaKeyBytes),
        "fitting key should return OK");
    assertEquals(KnownHosts.OK, kh.check("[192.277.325.5]:123", dsaKeyBytes),
        "fitting key should return OK");
    assertEquals(KnownHosts.NOT_INCLUDED, kh.check("[192.277.325.5:123]", dsaKeyBytes),
        "invalid syntax should return NOT_INCLUDED");
    assertEquals(KnownHosts.NOT_INCLUDED, kh.check("[]:123", dsaKeyBytes),
        "invalid syntax should return NOT_INCLUDED");

    assertEquals(KnownHosts.CHANGED,
        kh.check("host.example.com", "    ssh-rsa1234".getBytes(ISO_8859_1)),
        "changed key should return CHANGED");
    assertEquals(KnownHosts.NOT_INCLUDED, kh.check("host2.example.com", rsaKeyBytes),
        "host mismatch should return NOT_INCLUDED");

    assertEquals(KnownHosts.NOT_INCLUDED, kh.check("192.277.325.5", rsaKeyBytes),
        "wrong key should return NOT_INCLUDED");
    assertEquals(KnownHosts.OK, kh.check("[192.277.325.5]:123", dsaKeyBytes),
        "fitting key should return OK");
    assertEquals(KnownHosts.OK, kh.check("[192.277.325.5]:123", rsaKeyBytes),
        "fitting key should return OK");

    assertEquals(0, messages.size(), "no messages expected: " + getMessagesAsString());
  }

  @Test
  public void testAddGetRemoveHostKeys() throws Exception {
    boolean[] throwException = new boolean[1];
    Session.random = new NotSoRandomRandom();
    KnownHosts kh = new KnownHosts(jsch) {
      @Override
      void sync() throws IOException {
        messages.add("sync");
        if (throwException[0]) {
          messages.add("throw exception");
          throw new RuntimeException("dummy re");
        }
      }
    };
    HostKey[] hosts;

    addHosts(kh);

    hosts = kh.getHostKey();
    assertEquals(3, hosts.length, "unexpected number of host keys");
    assertEquals(
        "host.example.com,192.277.325.3: key type ssh-rsa\r\n"
            + "192.277.325.5: key type ssh-dss\r\n" + "[192.277.325.5]:123: key type ssh-rsa",
        getHostKeysString(hosts), "unexpected hosts");

    hosts = kh.getHostKey("nohost.example.com", null);
    assertEquals(0, hosts.length, "unexpected number of host keys");

    hosts = kh.getHostKey("host.example.com", null);
    assertEquals(1, hosts.length, "unexpected number of host keys");
    assertEquals("host.example.com,192.277.325.3: key type ssh-rsa", getHostKeysString(hosts),
        "unexpected hosts");

    hosts = kh.getHostKey("192.277.325.5", null);
    assertEquals(1, hosts.length, "unexpected number of host keys");
    assertEquals("192.277.325.5: key type ssh-dss", getHostKeysString(hosts), "unexpected hosts");

    hosts = kh.getHostKey("[192.277.325.5]:123", null);
    assertEquals(2, hosts.length, "unexpected number of host keys");
    assertEquals("[192.277.325.5]:123: key type ssh-rsa\r\n" + "192.277.325.5: key type ssh-dss",
        getHostKeysString(hosts), "unexpected hosts");

    hosts = kh.getHostKey("[192.277.325.5]:123", "ssh-dss");
    assertEquals(1, hosts.length, "unexpected number of host keys");
    assertEquals("192.277.325.5: key type ssh-dss", getHostKeysString(hosts), "unexpected hosts");

    hosts = kh.getHostKey("[192.277.325.5:123]", "ssh-dss");
    assertEquals(0, hosts.length, "unexpected number of host keys");

    assertEquals(0, messages.size(), "unexpected number of messages: " + getMessagesAsString());

    kh.remove(null, null);
    hosts = kh.getHostKey();
    assertEquals(0, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals("sync", getMessagesAsString(), "unexpected messages");

    addHosts(kh);
    kh.remove(null, "ssh-dsa");
    hosts = kh.getHostKey();
    assertEquals(0, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals("sync", getMessagesAsString(), "unexpected messages");

    addHosts(kh);
    kh.remove("host.example.com", null);
    hosts = kh.getHostKey();
    assertEquals(3, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals(
        "192.277.325.3: key type ssh-rsa\r\n" + "192.277.325.5: key type ssh-dss\r\n"
            + "[192.277.325.5]:123: key type ssh-rsa",
        getHostKeysString(hosts), "unexpected hosts");
    assertEquals("sync", getMessagesAsString(), "unexpected messages");

    kh.remove("[192.277.325.5]:123", "ssh-dsa");
    hosts = kh.getHostKey();
    assertEquals(3, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals(
        "192.277.325.3: key type ssh-rsa\r\n" + "192.277.325.5: key type ssh-dss\r\n"
            + "[192.277.325.5]:123: key type ssh-rsa",
        getHostKeysString(hosts), "unexpected hosts");
    assertEquals("", getMessagesAsString(), "unexpected messages");

    kh.remove("[192.277.325.5]:123", "ssh-rsa", "    ssh-rsa1234".getBytes(ISO_8859_1));
    hosts = kh.getHostKey();
    assertEquals(3, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals(
        "192.277.325.3: key type ssh-rsa\r\n" + "192.277.325.5: key type ssh-dss\r\n"
            + "[192.277.325.5]:123: key type ssh-rsa",
        getHostKeysString(hosts), "unexpected hosts");
    assertEquals("", getMessagesAsString(), "unexpected messages");

    kh.remove("[192.277.325.5]:123", "ssh-rsa", rsaKeyBytes);
    hosts = kh.getHostKey();
    assertEquals(2, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals("192.277.325.3: key type ssh-rsa\r\n" + "192.277.325.5: key type ssh-dss",
        getHostKeysString(hosts), "unexpected hosts");
    assertEquals("sync", getMessagesAsString(), "unexpected messages");

    HashedHostKey hhk = kh.new HashedHostKey("hashed.example.com", rsaKeyBytes);
    kh.add(hhk, null);
    hosts = kh.getHostKey();
    assertEquals(3, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals("192.277.325.3: key type ssh-rsa\r\n" + "192.277.325.5: key type ssh-dss\r\n"
        + "hashed.example.com: key type ssh-rsa", getHostKeysString(hosts), "unexpected hosts");
    assertEquals("", getMessagesAsString(), "unexpected messages");

    kh.remove("nothashed.example.com", "ssh-rsa");
    hosts = kh.getHostKey();
    assertEquals(3, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals("192.277.325.3: key type ssh-rsa\r\n" + "192.277.325.5: key type ssh-dss\r\n"
        + "hashed.example.com: key type ssh-rsa", getHostKeysString(hosts), "unexpected hosts");
    assertEquals("", getMessagesAsString(), "unexpected messages");

    kh.remove("hashed.example.com", "ssh-rsa");
    hosts = kh.getHostKey();
    assertEquals(2, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals("192.277.325.3: key type ssh-rsa\r\n" + "192.277.325.5: key type ssh-dss",
        getHostKeysString(hosts), "unexpected hosts");
    assertEquals("sync", getMessagesAsString(), "unexpected messages");

    hhk.hash();
    kh.add(hhk, null);
    hosts = kh.getHostKey();
    assertEquals(3, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals(
        "192.277.325.3: key type ssh-rsa\r\n" + "192.277.325.5: key type ssh-dss\r\n"
            + "|1|AAECAwQFBgcICQoLDA0ODxAREhM=|tfTk2zfUwEOJq8/nQE8s/gLfc58=: key type ssh-rsa",
        getHostKeysString(hosts), "unexpected hosts");
    assertEquals("", getMessagesAsString(), "unexpected messages");

    kh.remove("nothashed.example.com", "ssh-rsa");
    hosts = kh.getHostKey();
    assertEquals(3, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals(
        "192.277.325.3: key type ssh-rsa\r\n" + "192.277.325.5: key type ssh-dss\r\n"
            + "|1|AAECAwQFBgcICQoLDA0ODxAREhM=|tfTk2zfUwEOJq8/nQE8s/gLfc58=: key type ssh-rsa",
        getHostKeysString(hosts), "unexpected hosts");
    assertEquals("", getMessagesAsString(), "unexpected messages");

    kh.remove("hashed.example.com", "ssh-rsa");
    hosts = kh.getHostKey();
    assertEquals(2, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals("192.277.325.3: key type ssh-rsa\r\n" + "192.277.325.5: key type ssh-dss",
        getHostKeysString(hosts), "unexpected hosts");
    assertEquals("sync", getMessagesAsString(), "unexpected messages");

    throwException[0] = true;
    kh.remove("192.277.325.5", "ssh-dss");
    hosts = kh.getHostKey();
    assertEquals(1, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals("192.277.325.3: key type ssh-rsa", getHostKeysString(hosts), "unexpected hosts");
    assertEquals("sync\r\n" + "throw exception", getMessagesAsString(), "unexpected messages");

    kh.remove(null, null);
    addHosts(kh);
    hosts = kh.getHostKey();
    assertEquals(3, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals(
        "host.example.com,192.277.325.3: key type ssh-rsa\r\n"
            + "192.277.325.5: key type ssh-dss\r\n" + "[192.277.325.5]:123: key type ssh-rsa",
        getHostKeysString(hosts), "unexpected hosts");
    assertEquals("sync\r\n" + "throw exception", getMessagesAsString(), "unexpected messages");

    kh.remove("192.277.325.5", null);
    hosts = kh.getHostKey("[192.277.325.5]:123", null);
    assertEquals(1, hosts.length, "unexpected number of host keys: " + getHostKeysString(hosts));
    assertEquals("[192.277.325.5]:123: key type ssh-rsa", getHostKeysString(hosts),
        "unexpected hosts");
  }

  private String getMessagesAsString() {
    try {
      return messages.stream().collect(Collectors.joining("\r\n"));
    } finally {
      messages.clear();
    }
  }

  private void addHosts(KnownHosts kh) throws JSchException {
    kh.add(new HostKey("host.example.com,192.277.325.3", rsaKeyBytes), null);
    kh.add(new HostKey("192.277.325.5", dsaKeyBytes), null);
    kh.add(new HostKey("[192.277.325.5]:123", rsaKeyBytes), null);
  }

  private String getHostKeysString(HostKey[] hosts) {
    return Arrays.stream(hosts).map(host -> host.getHost() + ": key type " + host.getType())
        .collect(Collectors.joining("\r\n"));
  }

  private Boolean checkMessageAndReturn(String expectedMessge, String actual, boolean ret) {
    assertEquals(expectedMessge, actual, "prompted message mismatch");
    return Boolean.valueOf(ret);
  }

  private void checkUnhashedHostKey(HashedHostKey hhk, String expectedMarker, String expctedHost,
      String expectedType, String expectedComment) {
    assertEquals(expctedHost, hhk.getHost(), "host mismatch");
    assertEquals(expectedMarker, hhk.getMarker(), "marker mismatch");
    assertEquals("SHA256:HrVwkmVuavnWeqlDAECi58hRNd/uYBm3Sxgd60ZIKEs", hhk.getFingerPrint(jsch),
        "fingerprint mismatch");
    assertEquals(expectedComment, hhk.getComment(), "comment mismatch");
    assertEquals("ICAgIHNzaC1kc2E=", hhk.getKey(), "key mismatch");
    assertEquals(expectedType, hhk.getType(), "type mismatch");
    assertFalse(hhk.isHashed(), "key should report itself unhashed");
    assertNull(hhk.salt, "salt should be null");
    assertNull(hhk.hash, "hash should be null");
  }

  private void checkSHA1HashResult(HashedHostKey hhk, String expectedMarker, String expectedType,
      String expectedComment) throws UnsupportedEncodingException {
    assertEquals("|1|AAECAwQFBgcICQoLDA0ODxAREhM=|/pE4peaossRYDRp6bEWa348eFLI=", hhk.getHost(),
        "host mismatch");
    assertEquals(expectedMarker, hhk.getMarker(), "marker mismatch");
    assertEquals("SHA256:HrVwkmVuavnWeqlDAECi58hRNd/uYBm3Sxgd60ZIKEs", hhk.getFingerPrint(jsch),
        "fingerprint mismatch");
    assertEquals(expectedComment, hhk.getComment(), "comment mismatch");
    assertEquals("ICAgIHNzaC1kc2E=", hhk.getKey(), "key mismatch");
    assertEquals(expectedType, hhk.getType(), "type mismatch");
    assertTrue(hhk.isHashed(), "key should report itself hashed");
    assertEquals("AAECAwQFBgcICQoLDA0ODxAREhM=",
        new String(Util.toBase64(hhk.salt, 0, hhk.salt.length, true), ISO_8859_1),
        "salt should be null");
    assertEquals("/pE4peaossRYDRp6bEWa348eFLI=",
        new String(Util.toBase64(hhk.hash, 0, hhk.hash.length, true), ISO_8859_1),
        "salt should be null");
  }

  private String getContent(String filename) throws IOException {
    try (FileInputStream fis = new FileInputStream(filename)) {
      StringBuilder sb = new StringBuilder();
      int read;
      while ((read = fis.read()) != -1) {
        sb.append((char) read);
      }
      return sb.toString();
    }
  }

  private void checkResultForKeyResult(HostKey[] keys, String rsaKey, String expectedHostResult,
      String expectedMarker) {
    assertNotNull(keys, "actual keys expected");
    assertEquals(1, keys.length, "1 key expected");
    HostKey key = keys[0];
    assertEquals("some comment", key.getComment(), "comment mismatch");
    assertEquals("SHA256:nThbg6kXUpJWGl7E1IGOCspRomTxdCARLviKw6E5SY8", key.getFingerPrint(jsch),
        "fingerprint mismatch");
    assertEquals(expectedHostResult, key.getHost(), "host mismatch");
    assertEquals(rsaKey, key.getKey(), "key mismatch");
    assertEquals(expectedMarker, key.getMarker(), "marker mismatch");
    assertEquals("ssh-rsa", key.getType(), "type mismatch");
  }

  private static class BCHMACSHA1 implements MAC {
    private final Mac mac;

    private BCHMACSHA1(Mac mac) {
      this.mac = mac;
    }

    @Override
    public void update(int foo) {
      mac.update((byte) foo);
    }

    @Override
    public void update(byte[] foo, int start, int len) {
      mac.update(foo, start, len);
    }

    @Override
    public void init(byte[] key) throws Exception {
      SecretKey k = new SecretKeySpec(key, "hmacsha1");
      mac.init(k);
    }

    @Override
    public String getName() {
      return mac.getAlgorithm();
    }

    @Override
    public int getBlockSize() {
      return mac.getMacLength();
    }

    @Override
    public void doFinal(byte[] buf, int offset) {
      try {
        mac.doFinal(buf, offset);
      } catch (ShortBufferException sbe) {
        throw new RuntimeException("unable to do final", sbe);
      }
    }
  }

  private static class NotSoRandomRandom extends com.jcraft.jsch.jce.Random {
    @Override
    public void fill(byte[] foo, int start, int len) {
      for (int i = 0; i < len; i++) {
        foo[i + start] = (byte) i;
      }
    }
  }

  private static class TestLogger implements Logger {
    private final LinkedList<String> messages;

    private TestLogger(LinkedList<String> messages) {
      this.messages = messages;
    }

    @Override
    public void log(int level, String message) {
      messages.add("M(" + level + "): " + message);
    }

    @Override
    public void log(int level, String message, Throwable cause) {
      if (cause != null) {
        message += "\r\n  " + cause.getClass().getName() + ": " + cause.getMessage();
      }
      messages.add("M(" + level + "): " + message);
    }

    @Override
    public boolean isEnabled(int level) {
      return true;
    }
  }
}