/*
* Copyright 2012 aquenos GmbH.
* All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html.
*/
package com.aquenos.scm.ssh.server;
import java.io.IOException;
import java.io.Reader;
import java.math.BigInteger;
import java.security.PublicKey;
import java.security.interfaces.DSAParams;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.DSAParameterSpec;
import java.util.Arrays;
import java.util.LinkedList;
import java.util.List;
import org.apache.mina.util.Base64;
/**
* Utility for reading a file in the OpenSSH authorized_keys format.
*
* @author Sebastian Marsching
*/
public class AuthorizedKeysReader {
private static class DSAPublicKeyImpl implements DSAPublicKey {
private static final long serialVersionUID = -6107973445753882071L;
private final BigInteger pub;
private final DSAParams params;
private DSAPublicKeyImpl(BigInteger p, BigInteger q, BigInteger g,
BigInteger pub) {
this.pub = pub;
this.params = new DSAParameterSpec(p, q, g);
}
@Override
public String getFormat() {
return null;
}
@Override
public byte[] getEncoded() {
return null;
}
@Override
public String getAlgorithm() {
return "DSA";
}
@Override
public DSAParams getParams() {
return params;
}
@Override
public BigInteger getY() {
return pub;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("DSA Public Key\n");
sb.append(" y: ");
sb.append(pub.toString(16));
sb.append("\n");
return sb.toString();
}
}
private static class RSAPublicKeyImpl implements RSAPublicKey {
private static final long serialVersionUID = -5914480141972018173L;
private final BigInteger exponent;
private final BigInteger modulus;
private RSAPublicKeyImpl(BigInteger modulus, BigInteger exponent) {
this.exponent = exponent;
this.modulus = modulus;
}
@Override
public BigInteger getModulus() {
return modulus;
}
@Override
public String getFormat() {
return null;
}
@Override
public byte[] getEncoded() {
return null;
}
@Override
public String getAlgorithm() {
return "RSA";
}
@Override
public BigInteger getPublicExponent() {
return exponent;
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("RSA Public Key\n");
sb.append(" modulus: ");
sb.append(modulus.toString(16));
sb.append("\n");
sb.append(" public exponent: ");
sb.append(exponent.toString(16));
sb.append("\n");
return sb.toString();
}
}
/**
* Reads a list of public keys from a file in the OpenSSH authorized_keys
* format. Lines that do not match the expected format or use an unsupported
* key type are silently ignored. Only SSH v2 RSA and DSA keys are
* supported.
*
* @param reader
* the reader to read the public keys from.
* @return list of public keys found in the file.
* @throws IOException
* if an I/O error occurs while reading the file.
*/
public static List<PublicKey> readAuthorizedKeys(Reader reader)
throws IOException {
LinkedList<PublicKey> publicKeys = new LinkedList<PublicKey>();
int c;
lineloop: do {
do {
c = reader.read();
if (!Character.isWhitespace((char) c)) {
break;
}
} while (c != -1);
// We have skipped the leading whitespace (and possibly empty
// lines).
// Now, we expect either the start of a comment, the parameters
// list, or the key type.
if (c == '#') {
// Comment, skip everything until the end of line
c = skipToLineBreak(reader, c);
continue lineloop;
}
// We apply quotation rules for the parameter list. If there is no
// parameter list and we are reading the key type, this does not
// hurt us.
StringBuilder sb = new StringBuilder();
boolean expectingStartOfKey = true;
boolean inKey = false;
boolean expectingStartOfValue = false;
boolean inValue = false;
boolean expectingNextParameter = false;
boolean inEscapeSequence = false;
while (c != -1) {
if (expectingStartOfKey) {
if (Character.isLetterOrDigit((char) c) || c == '-') {
// First character of key.
sb.append((char) c);
expectingStartOfKey = false;
inKey = true;
} else {
// Malformed parameter list, so we skip the current
// line.
c = skipToLineBreak(reader, c);
continue lineloop;
}
} else if (inKey) {
if (c == ',') {
// Key without value ended, so expect next key.
sb.append((char) c);
inKey = false;
expectingStartOfKey = true;
} else if (c == '=') {
// Key with value ended, so expect value.
sb.append((char) c);
inKey = false;
expectingStartOfValue = true;
} else if (Character.isWhitespace((char) c)) {
// End of parameter list, so exit loop.
break;
} else if (Character.isLetterOrDigit((char) c) || c == '-') {
// Part of key.
sb.append((char) c);
} else {
// Malformed key, so skip line.
c = skipToLineBreak(reader, c);
continue lineloop;
}
} else if (expectingStartOfValue) {
if (c == '\"') {
sb.append((char) c);
expectingStartOfValue = false;
inValue = true;
} else {
// Malformed value, so skip line.
c = skipToLineBreak(reader, c);
continue lineloop;
}
} else if (inValue) {
if (inEscapeSequence) {
sb.append((char) c);
inEscapeSequence = false;
} else {
if (isLineBreak((char) c)) {
// Malformed value, so skip line.
continue lineloop;
} else if (c == '\\') {
sb.append((char) c);
inEscapeSequence = true;
} else if (c == '"') {
sb.append((char) c);
inValue = false;
expectingNextParameter = true;
} else {
// Normal part of value.
sb.append((char) c);
}
}
} else if (expectingNextParameter) {
if (c == ',') {
sb.append((char) c);
expectingNextParameter = false;
expectingStartOfKey = true;
} else if (Character.isWhitespace((char) c)) {
// End of parameter list.
sb.append((char) c);
break;
}
}
c = reader.read();
}
String parameterList = sb.toString();
// Reset string builder.
sb.setLength(0);
String keyType = null;
if (isSupportedKeyType(parameterList)) {
// The text we just read was not the parameter list, but the key
// type.
keyType = parameterList;
parameterList = null;
} else {
// Read key type.
c = skipWhitespaceNoLineBreak(reader);
while (c != -1) {
if (Character.isLetterOrDigit((char) c) || c == '-') {
sb.append((char) c);
} else if (Character.isWhitespace((char) c)) {
break;
} else {
// Malformed key type, skip line.
c = skipToLineBreak(reader, c);
continue lineloop;
}
}
keyType = sb.toString();
// Reset string builder.
sb.setLength(0);
if (!isSupportedKeyType(keyType)) {
// Unsupported key type, skip line.
c = skipToLineBreak(reader, c);
continue lineloop;
}
}
// Now we expect the public key (possibly separated by more
// whitespace).
c = skipWhitespaceNoLineBreak(reader);
while (c != -1) {
if (Character.isLetterOrDigit((char) c) || c == '+' || c == '/'
|| c == '=') {
sb.append((char) c);
} else if (Character.isWhitespace((char) c)) {
// End of public key.
break;
} else {
// Malformed key, skip line.
c = skipToLineBreak(reader, c);
continue lineloop;
}
c = reader.read();
}
String keyString = sb.toString();
// Reset string builder.
sb.setLength(0);
// Finally, there can be a comment, but we just skip the rest of the
// line.
c = skipToLineBreak(reader, c);
PublicKey pubKey;
try {
pubKey = readPublicKey(keyType, keyString);
} catch (IllegalArgumentException e) {
// Malformed key, so skip to next line
continue lineloop;
}
publicKeys.add(pubKey);
} while (c != -1);
return publicKeys;
}
private static boolean isLineBreak(char c) {
return c == '\r' || c == '\n';
}
private static int skipWhitespaceNoLineBreak(Reader reader)
throws IOException {
int c;
do {
c = reader.read();
} while (c != -1 && Character.isWhitespace((char) c)
&& !isLineBreak((char) c));
return c;
}
private static int skipToLineBreak(Reader reader, int c) throws IOException {
while (c != -1 && !isLineBreak((char) c)) {
c = reader.read();
}
return c;
}
private static boolean isSupportedKeyType(String keyType) {
return keyType.equals("ssh-rsa") || keyType.equals("ssh-dss");
}
private static PublicKey readPublicKey(String keyType, String keyString) {
if (keyType.equals("ssh-rsa")) {
return readRSAPublicKey(keyString);
} else if (keyType.equals("ssh-dss")) {
return readDSAPublicKey(keyString);
} else {
throw new IllegalArgumentException("Unsupported key type: "
+ keyType);
}
}
private static RSAPublicKey readRSAPublicKey(String keyString) {
byte[] keyData = Base64.decodeBase64(keyString.getBytes());
int keyLength = keyData.length;
int i = 0;
if (keyLength - i < 4) {
throw new IllegalArgumentException("Unexpected end of data.");
}
int fieldLength = readPositiveInt(keyData, i);
i += 4;
if (keyLength - i < fieldLength) {
throw new IllegalArgumentException("Unexpected end of data.");
}
String keyType = new String(keyData, i, fieldLength);
i += fieldLength;
if (!keyType.equals("ssh-rsa")) {
throw new IllegalArgumentException(
"Specified key is not a valid SSH RSA public key.");
}
if (keyLength - i < 4) {
throw new IllegalArgumentException("Unexpected end of data.");
}
fieldLength = readPositiveInt(keyData, i);
i += 4;
if (keyLength - i < fieldLength) {
throw new IllegalArgumentException("Unexpected end of data.");
}
final BigInteger exponent = new BigInteger(Arrays.copyOfRange(keyData,
i, i + fieldLength));
i += fieldLength;
if (keyLength - i < 4) {
throw new IllegalArgumentException("Unexpected end of data.");
}
fieldLength = readPositiveInt(keyData, i);
i += 4;
if (keyLength - i < fieldLength) {
throw new IllegalArgumentException("Unexpected end of data.");
}
final BigInteger modulus = new BigInteger(Arrays.copyOfRange(keyData,
i, i + fieldLength));
i += fieldLength;
if (i < keyLength) {
throw new IllegalArgumentException(
"Unexpcted surplus data at end of key.");
}
return new RSAPublicKeyImpl(modulus, exponent);
}
private static DSAPublicKey readDSAPublicKey(String keyString) {
byte[] keyData = Base64.decodeBase64(keyString.getBytes());
int keyLength = keyData.length;
int i = 0;
if (keyLength - i < 4) {
throw new IllegalArgumentException("Unexpected end of data.");
}
int fieldLength = readPositiveInt(keyData, i);
i += 4;
if (keyLength - i < fieldLength) {
throw new IllegalArgumentException("Unexpected end of data.");
}
String keyType = new String(keyData, i, fieldLength);
i += fieldLength;
if (!keyType.equals("ssh-dss")) {
throw new IllegalArgumentException(
"Specified key is not a valid SSH DSA public key.");
}
if (keyLength - i < 4) {
throw new IllegalArgumentException("Unexpected end of data.");
}
fieldLength = readPositiveInt(keyData, i);
i += 4;
if (keyLength - i < fieldLength) {
throw new IllegalArgumentException("Unexpected end of data.");
}
final BigInteger p = new BigInteger(Arrays.copyOfRange(keyData, i, i
+ fieldLength));
i += fieldLength;
if (keyLength - i < 4) {
throw new IllegalArgumentException("Unexpected end of data.");
}
fieldLength = readPositiveInt(keyData, i);
i += 4;
if (keyLength - i < fieldLength) {
throw new IllegalArgumentException("Unexpected end of data.");
}
final BigInteger q = new BigInteger(Arrays.copyOfRange(keyData, i, i
+ fieldLength));
i += fieldLength;
if (keyLength - i < 4) {
throw new IllegalArgumentException("Unexpected end of data.");
}
fieldLength = readPositiveInt(keyData, i);
i += 4;
if (keyLength - i < fieldLength) {
throw new IllegalArgumentException("Unexpected end of data.");
}
final BigInteger g = new BigInteger(Arrays.copyOfRange(keyData, i, i
+ fieldLength));
i += fieldLength;
if (keyLength - i < 4) {
throw new IllegalArgumentException("Unexpected end of data.");
}
fieldLength = readPositiveInt(keyData, i);
i += 4;
if (keyLength - i < fieldLength) {
throw new IllegalArgumentException("Unexpected end of data.");
}
final BigInteger pub = new BigInteger(Arrays.copyOfRange(keyData, i, i
+ fieldLength));
i += fieldLength;
if (i < keyLength) {
throw new IllegalArgumentException(
"Unexpcted surplus data at end of key.");
}
return new DSAPublicKeyImpl(p, q, g, pub);
}
private static int readPositiveInt(byte[] data, int offset) {
int result = readInt(data, offset);
if (result < 0) {
throw new IllegalArgumentException(
"Read negative number, where positive one was expected.");
}
return result;
}
private static int readInt(byte[] data, int offset) {
// Byte 0 is MSB
int byte0 = data[offset] & 0xFF;
int byte1 = data[offset + 1] & 0xFF;
int byte2 = data[offset + 2] & 0xFF;
int byte3 = data[offset + 3] & 0xFF;
int result = (byte0 << 24) | (byte1 << 16) | (byte2 << 8) | byte3;
return result;
}
}