/*
* (C) Copyright IBM Corp. 2009
*
* LICENSE: Eclipse Public License v1.0
* http://www.eclipse.org/legal/epl-v10.html
*/
package com.ibm.gaiandb;
import java.security.InvalidKeyException;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.Arrays;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import sun.misc.BASE64Decoder;
import com.ibm.gaiandb.diags.GDBMessages;
import com.ibm.gaiandb.tools.SQLDerbyRunner;
public class SecurityManager {
private static final boolean IS_SECURITY_EXCLUDED_FROM_RELEASE = GaianNode.IS_SECURITY_EXCLUDED_FROM_RELEASE;
// Use PROPRIETARY notice if class contains a main() method, otherwise use COPYRIGHT notice.
public static final String COPYRIGHT_NOTICE = "(c) Copyright IBM Corp. 2009";
private static final Logger logger = new Logger( "SecurityManager", 25 );
public static final String CREDENTIALS_LABEL = "GDB_CREDENTIALS";
public static final int ENCRYPTED_BLOCK_NUMBYTES_RSA = 64;
public static final String KEY_ALGORITHM_RSA = "RSA";
private static final int KEY_NUMBITS_RSA = 512;
public static final String CHECKSUM_ALGORITHM_MD5 = "MD5"; // Confirms a signature - hash takes 128 bits
public static final String CHECKSUM_ALGORITHM_SHA1 = "SHA1"; // Much harder to derive hash collisions, but takes 160 bits
private static KeyPair keyPair = null;
//private static KeyFactory keyFactory = null;
private static Cipher cipher = null;
/**
* Method used to retrieve the public key for this server. The key is typically sent to clients for encryption of their data.
*
* @return A serialized public key
* @throws SQLException
*/
public static byte[] getPublicKey() throws SQLException {
if ( IS_SECURITY_EXCLUDED_FROM_RELEASE ) return null;
if ( null == keyPair || null == cipher )
try{ initKeysAndCipher( KEY_ALGORITHM_RSA, KEY_NUMBITS_RSA ); }
catch (Exception e) { throw new SQLException("Internal Error: " + e); }
return keyPair.getPublic().getEncoded();
}
// public static void initKeysAndCipher(String keyAlgorithm) throws NoSuchAlgorithmException, NoSuchPaddingException {
// initKeysAndCipher(keyAlgorithm, KEY_NUMBITS_RSA);
// }
private static void initKeysAndCipher(String keyAlgorithm, int numBits) throws NoSuchAlgorithmException, NoSuchPaddingException {
// Get the public/private key pair and a Cipher instance for encrypting/decrypting
KeyPairGenerator keyGen = KeyPairGenerator.getInstance(keyAlgorithm);
keyGen.initialize(numBits);
keyPair = keyGen.genKeyPair();
cipher = Cipher.getInstance(keyAlgorithm);
}
// /**
// * Encrypt the given byte[] with the local static public key - this would not normally be used on the server side.
// *
// * @param decrypted
// * @param publicKey
// * @return
// * @throws InvalidKeyException
// * @throws IllegalBlockSizeException
// * @throws BadPaddingException
// * @throws NoSuchAlgorithmException
// * @throws NoSuchPaddingException
// */
// public static byte[] encrypt( byte[] decrypted, Key publicKey ) throws InvalidKeyException, IllegalBlockSizeException,
// BadPaddingException, NoSuchAlgorithmException, NoSuchPaddingException {
//
// if ( null == cipher ) cipher = Cipher.getInstance(KEY_ALGORITHM_RSA);
//
// cipher.init(Cipher.ENCRYPT_MODE, publicKey);
//// System.out.println("Max encrypted byte[] length: " + cipher.getOutputSize( decrypted.length ) );
// byte[] encrypted = cipher.doFinal(decrypted);
//
// return encrypted;
// }
/**
* Use the local static generated private key to decrypt a message that was presumably encrypted by the associated public key.
* If the message cannot be decrypted, the method will either throw an Exception or return garbage.
*
* @param encrypted
* @return
* @throws InvalidKeyException
* @throws IllegalBlockSizeException
* @throws BadPaddingException
* @throws NoSuchAlgorithmException
* @throws NoSuchPaddingException
*/
private static byte[] decryptUsingUniqueLocalPrivateKey( byte[] encrypted ) throws Exception {
if ( null == keyPair || null == cipher )
throw new Exception("Failed to decrypt bytes: Keys have not been generated (getPublicKey() was never called)");
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
byte[] decrypted = cipher.doFinal(encrypted);
return decrypted;
}
// /**
// * De-serializes a public key
// *
// * @param publicKeyBytes
// * @return
// * @throws NoSuchAlgorithmException
// * @throws InvalidKeySpecException
// */
// public static PublicKey deriveRSAPublicKey( byte[] publicKeyBytes )
// throws NoSuchAlgorithmException, InvalidKeySpecException {
// if ( null == keyFactory ) keyFactory = KeyFactory.getInstance(KEY_ALGORITHM_RSA);
// return keyFactory.generatePublic( new X509EncodedKeySpec(publicKeyBytes) );
// }
//
// /**
// * De-serializes a private key - should not normally be used, unless the private key has been passed around securely itself.
// *
// * @param privateKeyBytes
// * @return
// * @throws NoSuchAlgorithmException
// * @throws InvalidKeySpecException
// */
// public static PrivateKey deriveRSAPrivateKey( byte[] privateKeyBytes )
// throws NoSuchAlgorithmException, InvalidKeySpecException {
// if ( null == keyFactory ) keyFactory = KeyFactory.getInstance(KEY_ALGORITHM_RSA);
// return keyFactory.generatePrivate( new PKCS8EncodedKeySpec(privateKeyBytes) );
// }
/**
* Returns a checksum for the given byte[], given a checksum algorithm such as CHECKSUM_ALGORITHM_MD5
*
* @param input
* @param algo
* @return
* @throws NoSuchAlgorithmException
*/
private static byte[] getChecksum( byte[] input, String algo ) throws NoSuchAlgorithmException {
MessageDigest checksum = MessageDigest.getInstance(algo);
return checksum.digest(input);
}
public static byte[] getChecksumSHA1( byte[] input ) throws NoSuchAlgorithmException {
return getChecksum( input, CHECKSUM_ALGORITHM_SHA1 );
}
public static byte[] getChecksumMD5( byte[] input ) throws NoSuchAlgorithmException {
return getChecksum( input, CHECKSUM_ALGORITHM_MD5 );
}
public static String[] verifyCredentials( String b64EncodedMultiEncryptedBlock/*, String sqlQueryIn*/ ) throws SQLException {
if ( IS_SECURITY_EXCLUDED_FROM_RELEASE ) return null;
String authenticatedUser = null;
// We have a credentials column value that was passed in - we need to check the query hash and authenticate the user.
try {
byte[] multiEncryptedColumn = new BASE64Decoder().decodeBuffer( b64EncodedMultiEncryptedBlock );
// System.out.println("multiencrypted value len: " + multiEncryptedColumn.length + ", EACH BLOCK SIZE: " + SecurityManager.ENCRYPTED_BLOCK_NUMBYTES_RSA);
for ( int i=0; i<=multiEncryptedColumn.length-ENCRYPTED_BLOCK_NUMBYTES_RSA; i+=ENCRYPTED_BLOCK_NUMBYTES_RSA ) {
byte[] credEncrypted = new byte[ENCRYPTED_BLOCK_NUMBYTES_RSA];
System.arraycopy(multiEncryptedColumn, i, credEncrypted, 0, ENCRYPTED_BLOCK_NUMBYTES_RSA);
byte[] credentials = null;
try { credentials = decryptUsingUniqueLocalPrivateKey(credEncrypted); }
catch (Exception e) { logger.logInfo("Credentials segment from block not recognised for this node: " + e); continue; }
byte[][] fields = new byte[3][];
int idx = 0, pos = 0; // each field is preceded by its size which occupies 1 byte
for ( byte[] f : fields ) {
int fsize = credentials[pos++];
f = new byte[fsize];
System.arraycopy(credentials, pos, f, 0, fsize);
fields[idx++] = f;
pos += fsize;
}
String usr = new String(fields[0]), pwd = new String(fields[1]);
// Don't bother with the 3rd field for now (checksum)
// byte[] originalChecksum = fields[2];
// logger.logInfo("SQL query in: " + sqlQueryIn);
// logger.logInfo("Checksums match: " + Arrays.equals( originalChecksum, getChecksum(sqlQueryIn.getBytes(), CHECKSUM_ALGORITHM_MD5)));
// if ( Arrays.equals( originalChecksum, getChecksum(sqlQueryIn.getBytes(), CHECKSUM_ALGORITHM_MD5) ) &&
// authenticateUser(usr, getChecksum(pwd.getBytes(), CHECKSUM_ALGORITHM_SHA1)) ) {
if ( authenticateUser( usr, getChecksumSHA1(pwd.getBytes()) ) ) {
authenticatedUser = usr;
logger.logInfo("Successfully authenticated user " + authenticatedUser);
break;
}
}
} catch (Exception e) {
e.printStackTrace();
String errmsg = "Could not extract user credentials from DataValueDescriptor: " + e;
logger.logThreadWarning(GDBMessages.ENGINE_CREDENTIALS_VERIFY_ERROR, "DERBY ERROR: " + errmsg);
throw new SQLException( errmsg );
}
if ( null == authenticatedUser )
logger.logInfo("Unable to authenticate a user from the credentials block");
return getUserFields(authenticatedUser);
}
private static final String GDB_USERS_TABLE = "GDB_USERS";
private static final String colUser = "gdbuser";
private static final String colAffiliation = "affiliation";
private static final String colClearance = "clearance";
private static final String colPassword = "password";
private static Connection dedicatedConnection = null;
private static PreparedStatement pstmtGetPwd = null;
private static PreparedStatement pstmtSetPwd = null;
private static PreparedStatement pstmtRegisterUser = null;
private static PreparedStatement pstmtRemoveUser = null;
private static PreparedStatement pstmtGetUserFields = null;
private static void establishConnection() throws SQLException {
if ( null == dedicatedConnection || dedicatedConnection.isClosed() ) {
// Get connection to admin data
// Best to use embedded driver connection... otherwise, would need to consider whether sslMode is set..
dedicatedConnection = GaianDBConfig.getEmbeddedDerbyConnection();
pstmtGetPwd = null;
pstmtSetPwd = null;
pstmtRegisterUser = null;
pstmtRemoveUser = null;
}
}
public static void initialiseUsersTableAndItsUpdateTrigger(SQLDerbyRunner sdr) throws Exception {
if ( IS_SECURITY_EXCLUDED_FROM_RELEASE ) return;
dedicatedConnection = sdr.getConnection();
establishConnection();
Statement stmt = dedicatedConnection.createStatement();
try {
if ( false == Util.isExistsDerbyTable( dedicatedConnection, null, GDB_USERS_TABLE ) ) {
stmt.execute("create table " + GDB_USERS_TABLE + "(" + colUser + " " + Util.TSTR +
", " + colAffiliation + " " + Util.TSTR + ", " + colClearance + " " + Util.TSTR +
", " + colPassword + " CHAR(20) FOR BIT DATA, PRIMARY KEY (" + colUser + "))");
}
} catch ( SQLException e ) {
logger.logWarning(GDBMessages.ENGINE_USER_TABLE_INIT_ERROR, "Could not create GDB_USERS table: " + e);
};
// try { stmt.execute("drop procedure RegisterUser"); } catch ( SQLException e ) {}
// stmt.execute("create procedure RegisterUser (" + colUser + " " + Util.TSTR + ", " + colPassword + " " + Util.TSTR + ")" +
// " PARAMETER STYLE JAVA LANGUAGE JAVA NO SQL EXTERNAL NAME 'com.ibm.gaiandb.GaianDBConfigProcedures.registerUser'");
final String gdbtrigger = "gdbtrigger";
final String gdbTriggerSQL = ""
+ "!DROP FUNCTION "+gdbtrigger+" ;!CREATE FUNCTION "+gdbtrigger+"(S "+Util.XSTR+") RETURNS INT"
+ " PARAMETER STYLE JAVA LANGUAGE JAVA NO SQL EXTERNAL NAME 'com.ibm.gaiandb.GaianDBConfigProcedures.setTriggerEvent'"
+ ";"
+ "!DROP TRIGGER GDB_USERS_UPDATED;"
+ " !CREATE TRIGGER GDB_USERS_UPDATED AFTER UPDATE ON GDB_USERS FOR EACH STATEMENT MODE DB2SQL"
+ " select 1 from new com.ibm.db2j.GaianQuery('select "+gdbtrigger+"(''GDB_USERS_UPDATED'') from sysibm.sysdummy1') GQ"
+ ";"
;
sdr.processSQLs( gdbTriggerSQL );
}
static void registerUser( String usr, String affiliation, String clearance, String pwd ) throws SQLException, NoSuchAlgorithmException {
establishConnection();
if ( null == pstmtRegisterUser )
pstmtRegisterUser = dedicatedConnection.prepareStatement("insert into " + GDB_USERS_TABLE + " values(?, ?, ?, ?)");
pstmtRegisterUser.setString(1, usr);
pstmtRegisterUser.setString(2, affiliation);
pstmtRegisterUser.setString(3, clearance);
pstmtRegisterUser.setBytes(4, getChecksumSHA1(pwd.getBytes()));
pstmtRegisterUser.execute();
}
static void removeUser( String usr ) throws SQLException {
establishConnection();
if ( null == pstmtRemoveUser )
pstmtRemoveUser = dedicatedConnection.prepareStatement("delete from " + GDB_USERS_TABLE + " where " + colUser + "=?");
pstmtRemoveUser.setString(1, usr);
pstmtRemoveUser.execute();
}
private static String[] getUserFields(String usr) throws SQLException {
establishConnection();
if ( null == pstmtGetUserFields )
pstmtGetUserFields = dedicatedConnection.prepareStatement(
"select " + colAffiliation + ", " + colClearance + " from " + GDB_USERS_TABLE + " where " + colUser + "=?");
pstmtGetUserFields.setString(1, usr);
ResultSet rs = pstmtGetUserFields.executeQuery();
if ( !rs.next() ) {
logger.logWarning(GDBMessages.ENGINE_USER_FIELDS_GET_ERROR, "Unable to extract user fields - no entry found for user: " + usr);
return null;
}
String[] userFields = new String[3];
userFields[0] = usr;
userFields[1] = rs.getString(1);
userFields[2] = rs.getString(2);
if ( rs.next() ) {
logger.logWarning(GDBMessages.ENGINE_USER_CREDENTIALS_ERROR, "Error case detected: more than one credentials entry was found for user: " + usr);
return null;
}
return userFields;
}
private static boolean authenticateUser( String usr, byte[] pwdHashToAuthenticate ) throws SQLException, NoSuchAlgorithmException {
establishConnection();
if ( null == pstmtGetPwd ) pstmtGetPwd = dedicatedConnection.prepareStatement(
"select " + colPassword + " from " + GDB_USERS_TABLE + " where " + colUser + "=?");
if ( null == pstmtSetPwd ) pstmtSetPwd = dedicatedConnection.prepareStatement(
"update " + GDB_USERS_TABLE + " set " + colPassword + "=? where " + colUser + "=?");
pstmtGetPwd.setString(1, usr);
pstmtGetPwd.execute();
ResultSet rs = pstmtGetPwd.getResultSet(); //executeQuery();
if ( !rs.next() ) {
logger.logWarning(GDBMessages.ENGINE_USER_NOT_FOUND, "User not found on local server: " + usr);
rs.close();
return false;
}
byte[] pwdHash = rs.getBytes(1);
rs.close();
if ( null == pwdHash || 0 == pwdHash.length ) {
logger.logWarning(GDBMessages.ENGINE_USER_PASSWORD_BLANK, "Setting password (currently blank) from incoming query for user " + usr);
pstmtSetPwd.setBytes(1, pwdHashToAuthenticate);
pstmtSetPwd.setString(2, usr);
pstmtSetPwd.execute();
} else if ( !Arrays.equals( pwdHash, pwdHashToAuthenticate ) ) {
logger.logWarning(GDBMessages.ENGINE_USER_PASSWORD_INCORRECT, "Incorrect password entered for user: " + usr);
return false;
}
return true;
}
}