/*
* Copyright 2007-2010 Sun Microsystems, Inc.
*
* This file is part of Project Darkstar Server.
*
* Project Darkstar Server is free software: you can redistribute it
* and/or modify it under the terms of the GNU General Public License
* version 2 as published by the Free Software Foundation and
* distributed hereunder to you.
*
* Project Darkstar Server is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* --
*/
package com.sun.sgs.impl.auth;
import com.sun.sgs.auth.Identity;
import com.sun.sgs.auth.IdentityAuthenticator;
import com.sun.sgs.auth.IdentityCredentials;
import com.sun.sgs.impl.kernel.StandardProperties;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.StreamTokenizer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashMap;
import java.util.Properties;
import javax.security.auth.login.AccountNotFoundException;
import javax.security.auth.login.CredentialException;
/**
* This simple implementation provides authentication based on a name and
* a password. It is intended only for development use.
* <p>
* The names and cooresponding passwords are provided through a file, which
* is read on startup and then never re-read. The file is named using the
* property <code>PASSWORD_FILE_PROPERTY</code>. The file is of a simple form
* that consists of one entry per line, where each entry has a name, some
* whitespace, a SHA-256 hashed password that is encoded via
* <code>encodeBytes</code>, and finally a newline.
*/
public class NamePasswordAuthenticator implements IdentityAuthenticator
{
/**
* The property used to define the password file location.
*/
public static final String PASSWORD_FILE_PROPERTY =
"com.sun.sgs.impl.auth.NamePasswordAuthenticator.PasswordFile";
/**
* The default name for the password file, relative to the app root.
*/
public static final String DEFAULT_FILE_NAME = "passwords";
// a fixed map from name to passowrd, loaded from the password file
private final HashMap<String, byte[]> passwordMap;
// the instance used to hash passwords
private MessageDigest digest;
/**
* Creates an instance of <code>NamePasswordAuthenticator</code>.
*
* @param properties the application's configuration properties
*
* @throws FileNotFoundException if the password file cannot be found
* @throws IOException if any error occurs reading the password file
* @throws NoSuchAlgorithmException if SHA-256 is not supported
*/
public NamePasswordAuthenticator(Properties properties)
throws IOException, NoSuchAlgorithmException
{
if (properties == null) {
throw new NullPointerException("Null properties not allowed");
}
// get the name of the password file
String passFile = properties.getProperty(PASSWORD_FILE_PROPERTY);
if (passFile == null) {
String root = properties.getProperty(StandardProperties.APP_ROOT);
passFile = root + File.separator + DEFAULT_FILE_NAME;
}
// load the passwords
FileInputStream in = new FileInputStream(passFile);
StreamTokenizer stok = new StreamTokenizer(new InputStreamReader(in));
stok.eolIsSignificant(false);
//stok.wordChars('0', '0' + 16);
passwordMap = new HashMap<String, byte[]>();
while (stok.nextToken() != StreamTokenizer.TT_EOF) {
String name = stok.sval;
if (stok.nextToken() == StreamTokenizer.TT_EOF) {
throw new IOException("Unexpected EOL at line " +
stok.lineno());
}
byte [] password = decodeBytes(stok.sval.getBytes("UTF-8"));
passwordMap.put(name, password);
}
// finally, create the digest we'll use to hash incoming passwords
digest = MessageDigest.getInstance("SHA-256");
}
/**
* Decodes an array of bytes that has been encoded by a call to
* <code>encodeBytes</code>. This results in the original binary
* representation. This is used to decode a hashed password from the
* password file.
*
* @param bytes an encoded array of bytes as provided by a call
* to <code>encodePassword</code>
*
* @return the original binary representation
*/
public static byte [] decodeBytes(byte [] bytes) {
byte [] decoded = new byte[bytes.length / 2];
for (int i = 0; i < decoded.length; i++) {
int encodedIndex = i * 2;
decoded[i] = (byte) (((bytes[encodedIndex] - 'a') << 4) +
(bytes[encodedIndex + 1] - 'a'));
}
return decoded;
}
/**
* Encodes an array of bytes in a form suitable for including in a text
* file. In this case, a very simple base-16 encoding is used. The
* original binary representation can be resolved by calling
* <code>decodeBytes</code>. This is used to turn a hashed password
* into a form suitable for the password file.
*
* @param bytes an array of bytes
*
* @return an encoding of the bytes in a form suitable for use in text
*/
public static byte [] encodeBytes(byte [] bytes) {
byte [] encoded = new byte[bytes.length * 2];
for (int i = 0; i < bytes.length; i++) {
int encodedIndex = i * 2;
encoded[encodedIndex] = (byte) (((bytes[i] & 0xF0) >> 4) + 'a');
encoded[encodedIndex + 1] = (byte) ((bytes[i] & 0x0F) + 'a');
}
return encoded;
}
/**
* {@inheritDoc}
*/
public String [] getSupportedCredentialTypes() {
return new String [] { NamePasswordCredentials.TYPE_IDENTIFIER };
}
/**
* {@inheritDoc}
* <p>
* The provided <code>IdentityCredentials</code> must be an instance
* of <code>NamePasswordCredentials</code>.
*
* @throws AccountNotFoundException if the identity is unknown
* @throws CredentialException if the credentials are invalid
*/
public Identity authenticateIdentity(IdentityCredentials credentials)
throws AccountNotFoundException, CredentialException
{
// make sure that we were given the right type of credentials
if (!(credentials instanceof NamePasswordCredentials)) {
throw new CredentialException("unsupported credentials");
}
NamePasswordCredentials npc = (NamePasswordCredentials) credentials;
// get the name, and make sure they have a password entry
String name = npc.getName();
byte [] validPass = passwordMap.get(name);
if (validPass == null) {
throw new AccountNotFoundException("Unknown user: " + name);
}
// hash the given password
byte [] pass = null;
synchronized (digest) {
digest.reset();
try {
pass = digest.digest((new String(npc.getPassword())).
getBytes("UTF-8"));
} catch (IOException ioe) {
throw new CredentialException("Could not get password: " +
ioe.getMessage());
}
}
// verify that the hashes match
if (!Arrays.equals(validPass, pass)) {
throw new CredentialException("Invalid credentials");
}
return new IdentityImpl(name);
}
}