Merge pull request #41 from hierynomus/known-hosts

OpenSSH Known hosts format re-implemented
This commit is contained in:
Shikhar Bhushan
2011-12-04 09:35:58 -08:00
4 changed files with 350 additions and 230 deletions

View File

@@ -48,7 +48,7 @@ public class ConsoleKnownHostsVerifier
} }
if (response.equalsIgnoreCase(YES)) { if (response.equalsIgnoreCase(YES)) {
try { try {
entries().add(new SimpleEntry(hostname, key)); entries().add(new SimpleEntry(null, hostname, KeyType.fromKey(key), key));
write(); write();
console.printf("Warning: Permanently added '%s' (%s) to the list of known hosts.\n", hostname, type); console.printf("Warning: Permanently added '%s' (%s) to the list of known hosts.\n", hostname, type);
} catch (IOException e) { } catch (IOException e) {
@@ -60,7 +60,7 @@ public class ConsoleKnownHostsVerifier
} }
@Override @Override
protected boolean hostKeyChangedAction(Entry entry, String hostname, PublicKey key) { protected boolean hostKeyChangedAction(HostEntry entry, String hostname, PublicKey key) {
final KeyType type = KeyType.fromKey(key); final KeyType type = KeyType.fromKey(key);
final String fp = SecurityUtils.getFingerprint(key); final String fp = SecurityUtils.getFingerprint(key);
final String path = getFile().getAbsolutePath(); final String path = getFile().getAbsolutePath();

View File

@@ -15,11 +15,7 @@
*/ */
package net.schmizz.sshj.transport.verification; package net.schmizz.sshj.transport.verification;
import net.schmizz.sshj.common.Base64; import net.schmizz.sshj.common.*;
import net.schmizz.sshj.common.Buffer;
import net.schmizz.sshj.common.IOUtils;
import net.schmizz.sshj.common.KeyType;
import net.schmizz.sshj.common.SSHException;
import net.schmizz.sshj.transport.mac.HMACSHA1; import net.schmizz.sshj.transport.mac.HMACSHA1;
import net.schmizz.sshj.transport.mac.MAC; import net.schmizz.sshj.transport.mac.MAC;
import org.slf4j.Logger; import org.slf4j.Logger;
@@ -31,7 +27,10 @@ import java.io.File;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.FileReader; import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.PublicKey; import java.security.PublicKey;
import java.security.spec.RSAPublicKeySpec;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
@@ -42,266 +41,348 @@ import java.util.List;
* @see <a href="http://nms.lcs.mit.edu/projects/ssh/README.hashed-hosts">Hashed hostnames spec</a> * @see <a href="http://nms.lcs.mit.edu/projects/ssh/README.hashed-hosts">Hashed hostnames spec</a>
*/ */
public class OpenSSHKnownHosts public class OpenSSHKnownHosts
implements HostKeyVerifier { implements HostKeyVerifier {
public static abstract class Entry { protected final Logger log = LoggerFactory.getLogger(getClass());
private KeyType type; protected final File khFile;
private PublicKey key; protected final List<HostEntry> entries = new ArrayList<HostEntry>();
private String sKey;
protected void init(PublicKey key) public OpenSSHKnownHosts(File khFile) throws IOException {
throws SSHException { this.khFile = khFile;
this.key = key; if (khFile.exists()) {
this.type = KeyType.fromKey(key); final BufferedReader br = new BufferedReader(new FileReader(khFile));
if (type == KeyType.UNKNOWN) try {
throw new SSHException("Unknown key type for key: " + key); // Read in the file, storing each line as an entry
} String line;
while ((line = br.readLine()) != null)
try {
HostEntry entry = EntryFactory.parseEntry(line);
if (entry != null) {
entries.add(entry);
}
} catch (SSHException ignore) {
log.debug("Bad line ({}): {} ", ignore.toString(), line);
}
} finally {
IOUtils.closeQuietly(br);
}
}
}
protected void init(String typeString, String keyString) public File getFile() {
throws SSHException { return khFile;
this.sKey = keyString; }
this.type = KeyType.fromString(typeString);
if (type == KeyType.UNKNOWN)
throw new SSHException("Unknown key type: " + typeString);
}
public KeyType getType() { @Override
return type; public boolean verify(final String hostname, final int port, final PublicKey key) {
} final KeyType type = KeyType.fromKey(key);
if (type == KeyType.UNKNOWN)
return false;
public PublicKey getKey() final String adjustedHostname = (port != 22) ? "[" + hostname + "]:" + port : hostname;
throws IOException {
if (key == null) {
key = new Buffer.PlainBuffer(Base64.decode(sKey)).readPublicKey();
}
return key;
}
protected String getKeyString() { for (HostEntry e : entries)
if (sKey == null) { try {
final Buffer.PlainBuffer buf = new Buffer.PlainBuffer().putPublicKey(key); if (e.appliesTo(type, adjustedHostname))
sKey = Base64.encodeBytes(buf.array(), buf.rpos(), buf.available()); return e.verify(key) || hostKeyChangedAction(e, adjustedHostname, key);
} } catch (IOException ioe) {
return sKey; log.error("Error with {}: {}", e, ioe);
} return false;
}
return hostKeyUnverifiableAction(adjustedHostname, key);
}
public String getLine() { protected boolean hostKeyUnverifiableAction(String hostname, PublicKey key) {
final StringBuilder line = new StringBuilder(); return false;
line.append(getHostPart()); }
line.append(" ").append(type.toString());
line.append(" ").append(getKeyString());
return line.toString();
}
@Override protected boolean hostKeyChangedAction(HostEntry entry, String hostname, PublicKey key) {
public String toString() { log.warn("Host key for `{}` has changed!", hostname);
return "KnownHostsEntry{host=" + getHostPart() + "; type=" + type + "}"; return false;
} }
protected abstract String getHostPart(); public List<HostEntry> entries() {
return entries;
}
public abstract boolean appliesTo(String host) private static final String LS = System.getProperty("line.separator");
throws IOException;
} public void write()
throws IOException {
final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(khFile));
try {
for (HostEntry entry : entries)
bos.write((entry.getLine() + LS).getBytes(IOUtils.UTF8));
} finally {
bos.close();
}
}
public static class SimpleEntry public static File detectSSHDir() {
extends Entry { final File sshDir = new File(System.getProperty("user.home"), ".ssh");
return sshDir.exists() ? sshDir : null;
}
private final List<String> hosts;
public SimpleEntry(String host, PublicKey key) /**
throws SSHException { * Each line in these files contains the following fields: markers
this(Arrays.asList(host), key); * (optional), hostnames, bits, exponent, modulus, comment. The fields are
} * separated by spaces.
* <p/>
* The marker is optional, but if it is present then it must be one of
* ``@cert-authority'', to indicate that the line contains a certification
* authority (CA) key, or ``@revoked'', to indicate that the key contained
* on the line is revoked and must not ever be accepted. Only one marker
* should be used on a key line.
* <p/>
* Hostnames is a comma-separated list of patterns (`*' and `?' act as
* wildcards); each pattern in turn is matched against the canonical host
* name (when authenticating a client) or against the user-supplied name
* (when authenticating a server). A pattern may also be preceded by `!' to
* indicate negation: if the host name matches a negated pattern, it is not
* accepted (by that line) even if it matched another pattern on the line.
* A hostname or address may optionally be enclosed within `[' and `]'
* brackets then followed by `:' and a non-standard port number.
* <p/>
* Alternately, hostnames may be stored in a hashed form which hides host
* names and addresses should the file's contents be disclosed. Hashed
* hostnames start with a `|' character. Only one hashed hostname may
* appear on a single line and none of the above negation or wildcard
* operators may be applied.
* <p/>
* Bits, exponent, and modulus are taken directly from the RSA host key;
* they can be obtained, for example, from /etc/ssh/ssh_host_key.pub. The
* optional comment field continues to the end of the line, and is not used.
* <p/>
* Lines starting with `#' and empty lines are ignored as comments.
*/
public static class EntryFactory {
public SimpleEntry(List<String> hosts, PublicKey key) public static HostEntry parseEntry(String line) throws IOException {
throws SSHException { if (isComment(line)) {
this.hosts = hosts; return new CommentEntry(line);
init(key); }
}
public SimpleEntry(String line) String[] split = line.split(" ");
throws SSHException { int i = 0;
final String[] parts = line.split(" "); Marker marker = getMarker(split[i]);
if (parts.length != 3) if (marker != null) {
throw new SSHException("Line parts not 3: " + line); i++;
hosts = Arrays.asList(parts[0].split(",")); }
init(parts[1], parts[2]);
}
@Override String hostnames = split[i++];
public boolean appliesTo(String host) { String sType = split[i++];
for (String h : hosts) KeyType type = KeyType.fromString(sType);
if (host.equals(h)) PublicKey key;
return true;
return false;
}
@Override if (isType(type)) {
protected String getHostPart() { String sKey = split[i++];
final StringBuilder sb = new StringBuilder(); key = getKey(sKey);
for (String host : hosts) { } else if (isBits(sType)) {
if (sb.length() > 0) // a host already in there type = KeyType.RSA;
sb.append(","); int bits = Integer.valueOf(sType);
sb.append(host); BigInteger e = new BigInteger(split[i++]);
} BigInteger n = new BigInteger(split[i++]);
return sb.toString(); try {
} final KeyFactory keyFactory = SecurityUtils.getKeyFactory("RSA");
key = keyFactory.generatePublic(new RSAPublicKeySpec(n, e));
} catch (Exception ex) {
logger.error("Error reading entry {}, could not create key", line, ex);
return null;
}
} else {
logger.error("Error reading entry {}, could not determine type", line);
return null;
}
} if (isHashed(hostnames)) {
return new HashedEntry(marker, hostnames, type, key);
} else {
return new SimpleEntry(marker, hostnames, type, key);
}
}
public static class HashedEntry private static PublicKey getKey(String sKey) throws IOException {
extends Entry { return new Buffer.PlainBuffer(Base64.decode(sKey)).readPublicKey();
}
private final MAC sha1 = new HMACSHA1(); private static boolean isBits(String type) {
try {
Integer.parseInt(type);
return true;
} catch (NumberFormatException e) {
return false;
}
}
private String salt; private static boolean isType(KeyType type) {
private byte[] saltyBytes; return type != KeyType.UNKNOWN;
}
private final String hashedHost; private static boolean isComment(String line) {
return line.isEmpty() || line.startsWith("#");
}
public HashedEntry(String host, PublicKey key) public static Marker getMarker(String line) {
throws IOException { if (line.equals("@cert-authority")) return Marker.CA_CERT;
{ if (line.equals("@revoked")) return Marker.REVOKED;
saltyBytes = new byte[sha1.getBlockSize()]; return null;
new java.util.Random().nextBytes(saltyBytes); }
}
this.hashedHost = hashHost(host);
init(key);
}
public HashedEntry(String line) public static boolean isHashed(String line) {
throws IOException { return line.startsWith("|1|");
final String[] parts = line.split(" "); }
if (parts.length != 3)
throw new SSHException("Line parts not 3: " + line);
hashedHost = parts[0];
{
final String[] hostParts = hashedHost.split("\\|");
if (hostParts.length != 4)
throw new SSHException("Unrecognized format for hashed hostname");
salt = hostParts[2];
}
init(parts[1], parts[2]);
}
@Override }
public boolean appliesTo(String host)
throws IOException {
return hashedHost.equals(hashHost(host));
}
private String hashHost(String host) public interface HostEntry {
throws IOException { boolean appliesTo(KeyType type, String host) throws IOException;
sha1.init(getSaltyBytes()); boolean verify(PublicKey key) throws IOException;
return "|1|" + getSalt() + "|" + Base64.encodeBytes(sha1.doFinal(host.getBytes(IOUtils.UTF8))); String getLine();
} }
private byte[] getSaltyBytes() public static class CommentEntry implements HostEntry {
throws IOException { private final String comment;
if (saltyBytes == null) {
saltyBytes = Base64.decode(salt);
}
return saltyBytes;
}
private String getSalt() { public CommentEntry(String comment) {
if (salt == null) { this.comment = comment;
salt = Base64.encodeBytes(saltyBytes); }
}
return salt;
}
@Override @Override
protected String getHostPart() { public boolean appliesTo(KeyType type, String host) {
return hashedHost; return false;
} }
} @Override
public boolean verify(PublicKey key) {
return false;
}
protected final Logger log = LoggerFactory.getLogger(getClass()); @Override
public String getLine() {
return comment;
}
}
protected final File khFile; public static abstract class AbstractEntry implements HostEntry {
protected final List<Entry> entries = new ArrayList<Entry>();
public OpenSSHKnownHosts(File khFile) protected final OpenSSHKnownHosts.Marker marker;
throws IOException { protected final KeyType type;
this.khFile = khFile; protected PublicKey key;
if (khFile.exists()) {
final BufferedReader br = new BufferedReader(new FileReader(khFile));
try {
// Read in the file, storing each line as an entry
String line;
while ((line = br.readLine()) != null)
try {
entries.add(isHashed(line) ? new HashedEntry(line) : new SimpleEntry(line));
} catch (SSHException ignore) {
log.debug("Bad line ({}): {} ", ignore.toString(), line);
}
} finally {
IOUtils.closeQuietly(br);
}
}
}
public File getFile() { public AbstractEntry(Marker marker, KeyType type, PublicKey key) {
return khFile; this.marker = marker;
} this.type = type;
this.key = key;
}
@Override @Override
public boolean verify(final String hostname, final int port, final PublicKey key) { public boolean verify(PublicKey key) throws IOException {
final KeyType type = KeyType.fromKey(key); return key.equals(this.key) && marker != Marker.REVOKED;
if (type == KeyType.UNKNOWN) }
return false;
final String adjustedHostname = (port != 22) ? "[" + hostname + "]:" + port : hostname; public String getLine() {
final StringBuilder line = new StringBuilder();
for (Entry e : entries) if (marker != null) line.append(marker.getMarkerString()).append(" ");
try {
if (e.getType() == type && e.appliesTo(adjustedHostname))
return key.equals(e.getKey()) || hostKeyChangedAction(e, adjustedHostname, key);
} catch (IOException ioe) {
log.error("Error with {}: {}", e, ioe);
return false;
}
return hostKeyUnverifiableAction(adjustedHostname, key);
}
protected boolean hostKeyUnverifiableAction(String hostname, PublicKey key) { line.append(getHostPart());
return false; line.append(" ").append(type.toString());
} line.append(" ").append(getKeyString());
return line.toString();
}
protected boolean hostKeyChangedAction(Entry entry, String hostname, PublicKey key) { private String getKeyString() {
log.warn("Host key for `{}` has changed!", hostname); final Buffer.PlainBuffer buf = new Buffer.PlainBuffer().putPublicKey(key);
return false; return Base64.encodeBytes(buf.array(), buf.rpos(), buf.available());
} }
public List<Entry> entries() { protected abstract String getHostPart();
return entries; }
}
private static final String LS = System.getProperty("line.separator"); public static class SimpleEntry extends AbstractEntry {
private List<String> hosts;
private String hostnames;
public void write() public SimpleEntry(Marker marker, String hostnames, KeyType type, PublicKey key) {
throws IOException { super(marker, type, key);
final BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream(khFile)); this.hostnames = hostnames;
try { hosts = Arrays.asList(hostnames.split(","));
for (Entry entry : entries) }
bos.write((entry.getLine() + LS).getBytes(IOUtils.UTF8));
} finally {
bos.close();
}
}
public static File detectSSHDir() { @Override
final File sshDir = new File(System.getProperty("user.home"), ".ssh"); protected String getHostPart() {
return sshDir.exists() ? sshDir : null; return hostnames;
} }
public static boolean isHashed(String line) { @Override
return line.startsWith("|1|"); public boolean appliesTo(KeyType type, String host) throws IOException {
} return type == this.type && hostnames.contains(host);
}
}
public static class HashedEntry extends AbstractEntry {
private final MAC sha1 = new HMACSHA1();
private String salt;
private byte[] saltyBytes;
private final String hashedHost;
public HashedEntry(Marker marker, String hash, KeyType type, PublicKey key) throws SSHException {
super(marker, type, key);
this.hashedHost = hash;
{
final String[] hostParts = hashedHost.split("\\|");
if (hostParts.length != 4)
throw new SSHException("Unrecognized format for hashed hostname");
salt = hostParts[2];
}
}
@Override
public boolean appliesTo(KeyType type, String host) throws IOException {
return this.type == type && hashedHost.equals(hashHost(host));
}
private String hashHost(String host) throws IOException {
sha1.init(getSaltyBytes());
return "|1|" + salt + "|" + Base64.encodeBytes(sha1.doFinal(host.getBytes(IOUtils.UTF8)));
}
private byte[] getSaltyBytes() throws IOException {
if (saltyBytes == null) {
saltyBytes = Base64.decode(salt);
}
return saltyBytes;
}
@Override
public String getLine() {
return null;
}
@Override
protected String getHostPart() {
return hashedHost;
}
}
public enum Marker {
CA_CERT("@cert-authority"), REVOKED("@revoked");
private final String sMarker;
Marker(String sMarker) {
this.sMarker = sMarker;
}
public String getMarkerString() {
return sMarker;
}
}
private static final Logger logger = LoggerFactory.getLogger(OpenSSHKnownHosts.class);
} }

View File

@@ -17,15 +17,23 @@ package net.schmizz.sshj.transport.verification;
import net.schmizz.sshj.util.KeyUtil; import net.schmizz.sshj.util.KeyUtil;
import org.junit.Before; import org.junit.Before;
import org.junit.Rule;
import org.junit.Test; import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import java.io.BufferedWriter;
import java.io.File; import java.io.File;
import java.io.FileWriter;
import java.io.IOException; import java.io.IOException;
import java.net.UnknownHostException; import java.net.UnknownHostException;
import java.security.GeneralSecurityException; import java.security.GeneralSecurityException;
import java.security.PublicKey; import java.security.PublicKey;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.instanceOf;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.assertTrue; import static org.junit.Assert.assertTrue;
import static org.junit.internal.matchers.IsCollectionContaining.hasItem;
public class OpenSSHKnownHostsTest { public class OpenSSHKnownHostsTest {
@@ -33,23 +41,53 @@ public class OpenSSHKnownHostsTest {
// BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-15.15t] %-5p %-30.30c{1} - %m%n"))); // BasicConfigurator.configure(new ConsoleAppender(new PatternLayout("%d [%-15.15t] %-5p %-30.30c{1} - %m%n")));
// } // }
private OpenSSHKnownHosts kh; @Rule
public TemporaryFolder temp = new TemporaryFolder();
@Before @Before
public void setUp() public void setUp()
throws IOException, GeneralSecurityException { throws IOException, GeneralSecurityException {
kh = new OpenSSHKnownHosts(new File("src/test/resources/known_hosts")); // kh = new OpenSSHKnownHosts(new File("src/test/resources/known_hosts"));
} }
@Test public File writeKnownHosts(String line) throws IOException {
public void testLocalhostEntry() File known_hosts = temp.newFile("known_hosts");
throws UnknownHostException, GeneralSecurityException { FileWriter fileWriter = new FileWriter(known_hosts);
BufferedWriter writer = new BufferedWriter(fileWriter);
writer.write(line);
writer.write("\r\n");
writer.flush();
writer.close();
return known_hosts;
}
} @Test
public void shouldAddCommentForEmptyLine() throws IOException {
File file = writeKnownHosts("");
OpenSSHKnownHosts openSSHKnownHosts = new OpenSSHKnownHosts(file);
assertThat(openSSHKnownHosts.entries().size(), equalTo(1));
assertThat(openSSHKnownHosts.entries().get(0), instanceOf(OpenSSHKnownHosts.CommentEntry.class));
}
@Test
public void shouldAddCommentForCommentLine() throws IOException {
File file = writeKnownHosts("# this is a comment");
OpenSSHKnownHosts openSSHKnownHosts = new OpenSSHKnownHosts(file);
assertThat(openSSHKnownHosts.entries().size(), equalTo(1));
assertThat(openSSHKnownHosts.entries().get(0), instanceOf(OpenSSHKnownHosts.CommentEntry.class));
}
//
// @Test
// public void testLocalhostEntry()
// throws UnknownHostException, GeneralSecurityException {
//
// }
//
@Test @Test
public void testSchmizzEntry() public void testSchmizzEntry()
throws UnknownHostException, GeneralSecurityException { throws IOException, GeneralSecurityException {
OpenSSHKnownHosts kh = new OpenSSHKnownHosts(new File("src/test/resources/known_hosts"));
final PublicKey key = KeyUtil final PublicKey key = KeyUtil
.newRSAPublicKey( .newRSAPublicKey(
"e8ff4797075a861db9d2319960a836b2746ada3da514955d2921f2c6a6c9895cbd557f604e43772b6303e3cab2ad82d83b21acdef4edb72524f9c2bef893335115acacfe2989bcbb2e978e4fedc8abc090363e205d975c1fdc35e55ba4daa4b5d5ab7a22c40f547a4a0fd1c683dfff10551c708ff8c34ea4e175cb9bf2313865308fa23601e5a610e2f76838be7ded3b4d3a2c49d2d40fa20db51d1cc8ab20d330bb0dadb88b1a12853f0ecb7c7632947b098dcf435a54566bcf92befd55e03ee2a57d17524cd3d59d6e800c66059067e5eb6edb81946b3286950748240ec9afa4389f9b62bc92f94ec0fba9e64d6dc2f455f816016a4c5f3d507382ed5d3365", "e8ff4797075a861db9d2319960a836b2746ada3da514955d2921f2c6a6c9895cbd557f604e43772b6303e3cab2ad82d83b21acdef4edb72524f9c2bef893335115acacfe2989bcbb2e978e4fedc8abc090363e205d975c1fdc35e55ba4daa4b5d5ab7a22c40f547a4a0fd1c683dfff10551c708ff8c34ea4e175cb9bf2313865308fa23601e5a610e2f76838be7ded3b4d3a2c49d2d40fa20db51d1cc8ab20d330bb0dadb88b1a12853f0ecb7c7632947b098dcf435a54566bcf92befd55e03ee2a57d17524cd3d59d6e800c66059067e5eb6edb81946b3286950748240ec9afa4389f9b62bc92f94ec0fba9e64d6dc2f455f816016a4c5f3d507382ed5d3365",

View File

@@ -1,3 +1,4 @@
schmizz.net,69.163.155.180 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ== schmizz.net,69.163.155.180 ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA6P9Hlwdahh250jGZYKg2snRq2j2lFJVdKSHyxqbJiVy9VX9gTkN3K2MD48qyrYLYOyGs3vTttyUk+cK++JMzURWsrP4piby7LpeOT+3Iq8CQNj4gXZdcH9w15Vuk2qS11at6IsQPVHpKD9HGg9//EFUccI/4w06k4XXLm/IxOGUwj6I2AeWmEOL3aDi+fe07TTosSdLUD6INtR0cyKsg0zC7Da24ixoShT8Oy3x2MpR7CY3PQ1pUVmvPkr79VeA+4qV9F1JM09WdboAMZgWQZ+XrbtuBlGsyhpUHSCQOya+kOJ+bYryS+U7A+6nmTW3C9FX4FgFqTF89UHOC7V0zZQ==
Above we have a plain line, Below we have a hashed line, This is a garbage line. # Above we have a plain line, Below we have a hashed line, Last is a v1 line, This is a garbage line.
|1|dy7xSefq6NmJms6AzANG3w45W28=|SSCTlHs4pZbc2uaRoPvjyEAHE1g= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAu64GJcCkdtckPGt8uKTyhG1ShT1Np1kh10eE49imQ4Nh9Y/IrSPzDtYUAazQ88ABc2NffuOKkdn2qtUwZ1ulfcdNfN3oTim3BiVHqa041pKG0L+onQe8Bo+CaG5KBLy/C24eNGM9EcfQvDQOnq1eD3lnR/l8fFckldzjfxZgar0yT9Bb3pwp50oN+1wSEINJEHOgMIW8kZBQmyNr/B+b7yX+Y1s1vuYIP/i4WimCVmkdi9G87Ga8w7GxKalRD2QOG6Xms2YWRQDN6M/MOn4tda3EKolbWkctEWcQf/PcVJffTH4Wv5f0RjVyrQv4ha4FZcNAv6RkRd9WkiCsiTKioQ== |1|dy7xSefq6NmJms6AzANG3w45W28=|SSCTlHs4pZbc2uaRoPvjyEAHE1g= ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAu64GJcCkdtckPGt8uKTyhG1ShT1Np1kh10eE49imQ4Nh9Y/IrSPzDtYUAazQ88ABc2NffuOKkdn2qtUwZ1ulfcdNfN3oTim3BiVHqa041pKG0L+onQe8Bo+CaG5KBLy/C24eNGM9EcfQvDQOnq1eD3lnR/l8fFckldzjfxZgar0yT9Bb3pwp50oN+1wSEINJEHOgMIW8kZBQmyNr/B+b7yX+Y1s1vuYIP/i4WimCVmkdi9G87Ga8w7GxKalRD2QOG6Xms2YWRQDN6M/MOn4tda3EKolbWkctEWcQf/PcVJffTH4Wv5f0RjVyrQv4ha4FZcNAv6RkRd9WkiCsiTKioQ==
test.com,1.1.1.1 2048 35 22017496617994656680820635966392838863613340434802393112245951008866692373218840197754553998457793202561151141246686162285550121243768846314646395880632789308110750881198697743542374668273149584280424505890648953477691795864456749782348425425954366277600319096366690719901119774784695056100331902394094537054256611668966698242432417382422091372756244612839068092471592121759862971414741954991375710930168229171638843329213652899594987626853020377726482288618521941129157643483558764875338089684351824791983007780922947554898825663693324944982594850256042689880090306493029526546183035567296830604572253312294059766327