blob: bfa9a8a5210a3e6b6bc87d0f0221b70de3d91d70 [file] [log] [blame]
/*
* 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.karaf.shell.ssh;
import org.apache.mina.util.Base64;
import org.apache.sshd.server.PublickeyAuthenticator;
import org.apache.sshd.server.session.ServerSession;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.*;
import java.math.BigInteger;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PublicKey;
import java.security.interfaces.DSAPublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.DSAPublicKeySpec;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.RSAPublicKeySpec;
import java.util.*;
/**
* A public key authenticator, which reads an OpenSSL2 <code>authorized_keys</code> file.
*/
public class KarafPublickeyAuthenticator implements PublickeyAuthenticator {
private static final transient Logger LOGGER = LoggerFactory.getLogger(KarafPublickeyAuthenticator.class);
private String authorizedKeys;
private boolean active;
public static final class AuthorizedKey {
private final String alias;
private final String format;
private final PublicKey publicKey;
public AuthorizedKey(String alias, String format, PublicKey publicKey) {
super();
this.alias = alias;
this.format = format;
this.publicKey = publicKey;
}
public String getAlias() {
return this.alias;
}
public String getFormat() {
return this.format;
}
public PublicKey getPublicKey() {
return this.publicKey;
}
}
private static final class PublicKeyComparator implements Comparator<PublicKey> {
public int compare(PublicKey a, PublicKey b) {
if (a instanceof DSAPublicKey) {
if (b instanceof DSAPublicKey) {
DSAPublicKey da = (DSAPublicKey) a;
DSAPublicKey db = (DSAPublicKey) b;
int r = da.getParams().getG().compareTo(db.getParams().getG());
if (r != 0) {
return r;
}
r = da.getParams().getP().compareTo(db.getParams().getP());
if (r != 0) {
return r;
}
r = da.getParams().getQ().compareTo(db.getParams().getQ());
if (r != 0) {
return r;
}
return da.getY().compareTo(db.getY());
} else {
return -1;
}
} else if (a instanceof RSAPublicKey) {
if (b instanceof RSAPublicKey) {
RSAPublicKey da = (RSAPublicKey) a;
RSAPublicKey db = (RSAPublicKey) b;
int r = da.getPublicExponent().compareTo(db.getPublicExponent());
if (r != 0) {
return r;
}
return da.getModulus().compareTo(db.getModulus());
} else {
return -1;
}
} else {
throw new IllegalArgumentException("Only RSA and DAS keys are supported.");
}
}
}
private final class AuthorizedKeysProvider extends TimerTask {
private Map<PublicKey, AuthorizedKey> keys;
private Long lastModificationDate;
private Boolean fileAvailable;
@Override
public void run() {
try {
File af = new File(KarafPublickeyAuthenticator.this.authorizedKeys);
if (af.exists()) {
Long newModificationDate = Long.valueOf(af.lastModified());
if ((this.fileAvailable != null && !this.fileAvailable.booleanValue()) || !newModificationDate.equals(this.lastModificationDate)) {
LOGGER.debug("Parsing authorized keys file {}...", KarafPublickeyAuthenticator.this.authorizedKeys);
this.fileAvailable = Boolean.TRUE;
this.lastModificationDate = newModificationDate;
Map<PublicKey, AuthorizedKey> newKeys = KarafPublickeyAuthenticator.parseAuthorizedKeys(new FileInputStream(af));
this.setKeys(newKeys);
LOGGER.debug("Successfully parsed {} keys from file {}", newKeys.size(), KarafPublickeyAuthenticator.this.authorizedKeys);
}
} else {
if (this.fileAvailable != null && this.fileAvailable.booleanValue()) {
LOGGER.debug("Authorized keys file {} disappeared, will recheck every minute", KarafPublickeyAuthenticator.this.authorizedKeys);
} else if (this.fileAvailable == null) {
LOGGER.debug("Authorized keys file {} does not exist, will recheck every minute", KarafPublickeyAuthenticator.this.authorizedKeys);
}
this.fileAvailable = Boolean.FALSE;
this.lastModificationDate = null;
this.setKeys(null);
}
} catch (Throwable e) {
LOGGER.error("Error parsing authorized keys file {}", KarafPublickeyAuthenticator.this.authorizedKeys, e);
this.fileAvailable = Boolean.FALSE;
this.lastModificationDate = null;
this.setKeys(null);
}
}
private synchronized void setKeys(Map<PublicKey, AuthorizedKey> keys) {
this.keys = keys;
}
public synchronized AuthorizedKey getKey(PublicKey publicKey) {
if (this.keys == null) {
return null;
}
return this.keys.get(publicKey);
}
}
private Timer parseAuthorizedKeysTimer;
private AuthorizedKeysProvider authorizedKeysProvider;
private static final int getInt(byte[] b, int pos) {
return (((int) b[pos] & 0xff) << 24) +
(((int) b[pos+1] & 0xff) << 16) +
(((int) b[pos+2] & 0xff) << 8) +
((int) b[pos+3] & 0xff);
}
/**
* Parse an <code>authorized_keys</code> file in OpenSSH style.
*
* @param is the input stream to read.
* @return a map of authorized public keys.
* @throws IOException
* @throws NoSuchAlgorithmException
* @throws InvalidKeySpecException
*/
public static final Map<PublicKey, AuthorizedKey> parseAuthorizedKeys(InputStream is) throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
try {
Base64 decoder = new Base64();
KeyFactory rsaKeyGen = KeyFactory.getInstance("RSA");
KeyFactory dsaKeyGen = KeyFactory.getInstance("DSA");
LineNumberReader reader = new LineNumberReader(new InputStreamReader(is, "UTF-8"));
Map<PublicKey, AuthorizedKey> ret = new TreeMap<PublicKey, AuthorizedKey>(new PublicKeyComparator());
String line;
while ((line = reader.readLine()) != null) {
String[] tokens = line.split("[ \\t]+", 3);
if (tokens.length != 3) {
throw new IOException("Authorized keys file line " + reader.getLineNumber() + " does not contain 3 tokens.");
}
byte[] rawKey = decoder.decode(tokens[1].getBytes("UTF-8"));
if (getInt(rawKey, 0) != 7 || !new String(rawKey, 4, 7, "UTF-8").equals(tokens[0])) {
throw new IOException("Authorized keys file line " + reader.getLineNumber() + " contains a key with a format that does not match the first token.");
}
PublicKey pk;
if (tokens[0].equals("ssh-dss")) {
int pos = 11;
int n = getInt(rawKey, pos);
pos += 4;
BigInteger p = new BigInteger(1, KarafPublickeyAuthenticator.arraysCopyOfRange(rawKey, pos, pos + n));
pos += n;
n = getInt(rawKey, pos);
pos += 4;
BigInteger q = new BigInteger(1, KarafPublickeyAuthenticator.arraysCopyOfRange(rawKey, pos, pos + n));
pos += n;
n = getInt(rawKey, pos);
pos += 4;
BigInteger g = new BigInteger(1, KarafPublickeyAuthenticator.arraysCopyOfRange(rawKey, pos, pos + n));
pos += n;
n = getInt(rawKey, pos);
pos += 4;
BigInteger y = new BigInteger(1, KarafPublickeyAuthenticator.arraysCopyOfRange(rawKey, pos, pos + n));
pos += n;
if (pos != rawKey.length) {
throw new IOException("Authorized keys file line " + reader.getLineNumber() + " contains a DSA key with extra garbage.");
}
DSAPublicKeySpec ps = new DSAPublicKeySpec(y, p, q, g);
pk = dsaKeyGen.generatePublic(ps);
} else if (tokens[0].equals("ssh-rsa")) {
int pos = 11;
int n = getInt(rawKey, pos);
pos += 4;
BigInteger e = new BigInteger(1, KarafPublickeyAuthenticator.arraysCopyOfRange(rawKey, pos, pos + n));
pos += n;
n = getInt(rawKey, pos);
pos += 4;
BigInteger modulus = new BigInteger(1, KarafPublickeyAuthenticator.arraysCopyOfRange(rawKey, pos, pos + n));
pos += n;
if (pos != rawKey.length) {
throw new IOException("Authorized keys file line " + reader.getLineNumber() + " contains a RSA key with extra garbage.");
}
RSAPublicKeySpec ps = new RSAPublicKeySpec(modulus, e);
pk = rsaKeyGen.generatePublic(ps);
} else {
throw new IOException("Authorized keys file line " + reader.getLineNumber() + " does not start with ssh-dss or ssh-rsa.");
}
ret.put(pk, new AuthorizedKey(tokens[2], tokens[0], pk));
}
return ret;
} finally {
is.close();
}
}
public boolean authenticate(String username, PublicKey publicKey, ServerSession session) {
AuthorizedKey ak = this.authorizedKeysProvider.getKey(publicKey);
if (ak == null) {
LOGGER.error("Failed authenticate of user {} from {} with unknown public key.", username, session.getIoSession().getRemoteAddress());
return false;
}
LOGGER.debug("Successful authentication of user {} from {} with public key {}.", new Object[]{ username, session.getIoSession().getRemoteAddress(), ak.getAlias() });
return true;
}
public void setAuthorizedKeys(String path) {
this.authorizedKeys = path;
}
public void setActive(boolean active) {
this.active = active;
}
public void startTimer() {
if (this.active) {
this.parseAuthorizedKeysTimer = new Timer();
this.authorizedKeysProvider = new AuthorizedKeysProvider();
this.parseAuthorizedKeysTimer.schedule(this.authorizedKeysProvider, 10, 60000L);
}
}
public void stopTimer() {
if (this.parseAuthorizedKeysTimer != null) {
this.parseAuthorizedKeysTimer.cancel();
this.parseAuthorizedKeysTimer = null;
}
}
private static byte[] arraysCopyOfRange(byte[] original, int from, int to) {
int newLength = to - from;
if (newLength < 0)
throw new IllegalArgumentException(from + " > " + to);
byte[] copy = new byte[newLength];
System.arraycopy(original, from, copy, 0,
Math.min(original.length - from, newLength));
return copy;
}
}