package org.myrobotlab.service;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.security.GeneralSecurityException;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.Properties;
import java.util.Scanner;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import org.myrobotlab.codec.CodecUtils;
import org.myrobotlab.framework.Message;
import org.myrobotlab.framework.Service;
import org.myrobotlab.framework.ServiceType;
import org.myrobotlab.io.FileIO;
import org.myrobotlab.logging.Level;
import org.myrobotlab.logging.LoggerFactory;
import org.myrobotlab.logging.Logging;
import org.myrobotlab.logging.LoggingFactory;
import org.myrobotlab.service.interfaces.AuthorizationProvider;
import org.myrobotlab.service.interfaces.ServiceInterface;
import org.slf4j.Logger;
// SINGLETON ??? similar to Runtime ???
// http://blog.palominolabs.com/2011/10/18/java-2-way-tlsssl-client-certificates-and-pkcs12-vs-jks-keystores/
// http://juliusdavies.ca/commons-ssl/ssl.html
// http://stackoverflow.com/questions/4319496/how-to-encrypt-and-decrypt-data-in-java
// controlling export is "nice" but its control messages are the most important to mediate
public class Security extends Service implements AuthorizationProvider {
public static class Group {
// TODO - single access login
// timestamp -
public String groupId;
public boolean defaultAccess = true;
public HashMap<String, Boolean> accessRules = new HashMap<String, Boolean>();
}
public static class User {
// timestamp - single access login
public String userId;
public String password; // encrypt
public String groupId; // support only 1 group now Yay !
}
private static final long serialVersionUID = 1L;
// TODO - concept (similar in Drupal) - anonymous, authenticated, admin ..
// default groups ?
transient private static final HashMap<String, Boolean> allowExportByName = new HashMap<String, Boolean>();
transient private static final HashMap<String, Boolean> allowExportByType = new HashMap<String, Boolean>();
public final static Logger log = LoggerFactory.getLogger(Security.class);
// many to 1 mapping - currently does not support many to many Yay !
// transient private static final HashMap <String,String> userToGroup = new
// HashMap <String,String>();
// transient private boolean defaultAccess = true;
// below is authorization
transient private static final HashMap<String, Group> groups = new HashMap<String, Group>();
// users only map to groups - groups have the only access rules
transient private static final HashMap<String, User> users = new HashMap<String, User>();
static private Properties store = new Properties();
static private String storeDirPath = String.format("%s%s.myrobotlab", System.getProperty("user.home"), File.separator);
static private String keyFileName = "key";
static private String storeFileName = "store";
private static boolean isLoaded = false;
transient private boolean defaultAllowExport = true;
private String defaultNewGroupId = "anonymous";
// private HashMap<String, byte[]> keys = new HashMap<String, byte[]>();
public static final String AES = "AES";
public static void addSecret(String name, String secret) {
store.put(name, secret);
saveStore();
}
// private HashMap<String, ByteArrayOutputStream> persistantStore = new
// HashMap<String, ByteArrayOutputStream>();
private static String byteArrayToHexString(byte[] b) {
StringBuffer sb = new StringBuffer(b.length * 2);
for (int i = 0; i < b.length; i++) {
int v = b[i] & 0xff;
if (v < 16) {
sb.append('0');
}
sb.append(Integer.toHexString(v));
}
return sb.toString().toUpperCase();
}
/*
* public boolean loadKeyStore(String location) {
*
* }
*/
/**
* decrypt a value
*
* @throws GeneralSecurityException
* @throws IOException
*/
public static String decrypt(String message, File keyFile) throws GeneralSecurityException, IOException {
SecretKeySpec sks = getSecretKeySpec(keyFile);
Cipher cipher = Cipher.getInstance(Security.AES);
cipher.init(Cipher.DECRYPT_MODE, sks);
byte[] decrypted = cipher.doFinal(hexStringToByteArray(message));
return new String(decrypted);
}
/**
* encrypt a value and generate a keyfile if the keyfile is not found then a
* new one is created
*
* @throws GeneralSecurityException
* @throws IOException
*/
public static String encrypt(String passphrase, File keyFile) throws GeneralSecurityException, IOException {
if (!keyFile.exists()) {
new File(keyFile.getParent()).mkdirs();
KeyGenerator keyGen = KeyGenerator.getInstance(Security.AES);
keyGen.init(128);
SecretKey sk = keyGen.generateKey();
FileWriter fw = new FileWriter(keyFile);
fw.write(byteArrayToHexString(sk.getEncoded()));
fw.flush();
fw.close();
}
SecretKeySpec sks = getSecretKeySpec(keyFile);
Cipher cipher = Cipher.getInstance(Security.AES);
cipher.init(Cipher.ENCRYPT_MODE, sks, cipher.getParameters());
byte[] encrypted = cipher.doFinal(passphrase.getBytes());
return byteArrayToHexString(encrypted);
}
static public String getKeyFileName() {
return String.format("%s%s%s", storeDirPath, File.separator, keyFileName);
}
public static String getSecret(String name) {
if (store.containsKey(name)) {
return store.getProperty(name);
}
log.error(String.format("could not find %s in security store", name));
return null;
}
private static SecretKeySpec getSecretKeySpec(File keyFile) throws NoSuchAlgorithmException, IOException {
byte[] key = readKeyFile(keyFile);
SecretKeySpec sks = new SecretKeySpec(key, Security.AES);
return sks;
}
static public String getStoreFileName() {
return String.format("%s%s%s", storeDirPath, File.separator, storeFileName);
}
// default group permissions - for new user/group
// anonymous
// authenticated
private static byte[] hexStringToByteArray(String s) {
byte[] b = new byte[s.length() / 2];
for (int i = 0; i < b.length; i++) {
int index = i * 2;
int v = Integer.parseInt(s.substring(index, index + 2), 16);
b[i] = (byte) v;
}
return b;
}
// TODO - error if already exists !!
// FIXME - errors if store has not been initalized
public static void initializeStore(String passphrase) {
try {
String keyfile = getKeyFileName();
log.info(String.format("initializing key file %s", keyfile));
encrypt(passphrase, new File(keyfile));
} catch (Exception e) {
Logging.logError(e);
}
}
static public void loadStore() {
try {
// FIXME - store not threadsafe
Properties fileStore = new Properties();
String storeFileContents = FileIO.toString(getStoreFileName());
if (storeFileContents != null) {
String properties = decrypt(storeFileContents, new File(getKeyFileName()));
ByteArrayInputStream bis = new ByteArrayInputStream(properties.getBytes());
fileStore.load(bis);
// memory has precedence over file
fileStore.putAll(store);
store = fileStore;
isLoaded = true;
}
} catch (Exception e) {
Logging.logError(e);
}
}
public static void main(String[] args) throws Exception {
LoggingFactory.init(Level.INFO);
final String KEY_FILE = "c:/tempPass/howto.key";
final String PWD_FILE = "c:/tempPass/howto.properties";
// initializeStore("im a rockin rocker");
loadStore();
log.info(Security.getSecret("xmpp.user"));
Security.addSecret("xmpp.user", "supertick@gmail.com");
Security.addSecret("xmpp.pwd", "mrlRocks!");
saveStore();
String clearPwd = "mrlRocks!";
Properties p1 = new Properties();
p1.put("webgui.user", "supertick@gmail.com");
p1.put("webgui.pwd", "zd7");
p1.put("xmpp.user", "supertick@gmail.com");
String encryptedPwd = Security.encrypt(clearPwd, new File(KEY_FILE));
p1.put("xmpp.pwd", encryptedPwd);
p1.store(new FileWriter(PWD_FILE), "");
// ==================
Properties p2 = new Properties();
p2.load(new FileReader(PWD_FILE));
encryptedPwd = p2.getProperty("xmpp.pwd");
System.out.println(encryptedPwd);
System.out.println(Security.decrypt(encryptedPwd, new File(KEY_FILE)));
}
private static byte[] readKeyFile(File keyFile) throws FileNotFoundException {
Scanner scanner = new Scanner(keyFile);
scanner.useDelimiter("\\Z");
String keyValue = scanner.next();
scanner.close();
return hexStringToByteArray(keyValue);
}
/*
* public static void main(String[] args) {
* LoggingFactory.getInstance().configure();
* LoggingFactory.getInstance().setLevel(Level.INFO);
*
* try {
*
* //Serializable s = new SerializableImage(null, null); // passphrase - key
* // A better way to create a key is with a SecretKeyFactory using a salt:
*
* String passphrase = "correct horse battery staple"; MessageDigest digest =
* MessageDigest.getInstance("SHA"); digest.update(passphrase.getBytes());
* SecretKeySpec key = new SecretKeySpec(digest.digest(), 0, 16, "AES");
*
* Cipher aes = Cipher.getInstance("AES/ECB/PKCS5Padding");
* aes.init(Cipher.ENCRYPT_MODE, key); byte[] ciphertext = aes.doFinal(
* "my cleartext".getBytes()); log.info(new String(ciphertext));
*
* aes.init(Cipher.DECRYPT_MODE, key); String cleartext = new
* String(aes.doFinal(ciphertext));
*
* log.info(cleartext);
*
* } catch (Exception e) { Logging.logException(e); }
*
* Security security = new Security("security"); security.startService();
*
* Runtime.createAndStart("gui", "GUIService");
*
* }
*/
static public void saveStore() {
try {
if (!isLoaded) {
loadStore();
}
ByteArrayOutputStream out = new ByteArrayOutputStream();
store.store(out, null);
String encrypted = Security.encrypt(new String(out.toByteArray()), new File(getKeyFileName()));
FileIO.toFile(getStoreFileName(), encrypted);
} catch (Exception e) {
Logging.logError(e);
}
}
public Security(String n) {
super(n);
createDefaultGroups();
/*
* FIXME - set predefined levels - high security medium low
* allowExportByType.put("XMPP", false);
* allowExportByType.put("RemoteAdapter", false);
* allowExportByType.put("WebGui", false);
* allowExportByType.put("GUIService", false);
*
* allowExportByType.put("Java", false); allowExportByType.put("Python",
* false);
*
* allowExportByType.put("Security", false);
* allowExportByType.put("Runtime", false);
*/
allowExportByType.put("Security", false);
setSecurityProvider(this);
}
public boolean addGroup(String groupId) {
return addGroup(groupId, false);
}
public boolean addGroup(String groupId, boolean defaultAccess) {
Group g = new Group();
g.groupId = groupId;
g.defaultAccess = defaultAccess;
if (groups.containsKey(groupId)) {
log.warn(String.format("group %s already exists", groupId));
return false;
}
groups.put(groupId, g);
return true;
}
public boolean addUser(String user) {
return addUser(user, null, null);
}
public boolean addUser(String userId, String password, String groupId) {
if (users.containsKey(userId)) {
log.warn(String.format("user %s already exists", userId));
return false;
}
User u = new User();
u.userId = userId;
u.password = password;
if (groupId == null) {
u.groupId = defaultNewGroupId;
} else {
u.groupId = groupId;
}
if (!groups.containsKey(u.groupId)) {
error("could not add user %s groupId %s does not exist", userId, groupId);
return false;
}
users.put(userId, u);
return true;
}
@Override
public boolean allowExport(String serviceName) {
if (allowExportByName.containsKey(serviceName)) {
return allowExportByName.get(serviceName);
}
ServiceInterface si = Runtime.getService(serviceName);
if (si == null) {
error("%s could not be found for export", serviceName);
return false;
}
String fullType = si.getClass().getSimpleName();
if (allowExportByType.containsKey(fullType)) {
return allowExportByType.get(fullType);
}
return defaultAllowExport;
}
public Boolean allowExportByName(String name, Boolean access) {
return allowExportByName.put(name, access);
}
public Boolean allowExportByType(String type, Boolean access) {
return allowExportByType.put(CodecUtils.type(type), access);
}
public void createDefaultGroups() {
Group g = new Group();
g.groupId = "anonymous";
g.defaultAccess = false;
groups.put("anonymous", g);
g = new Group();
g.groupId = "authenticated";
g.defaultAccess = true;
groups.put("authenticated", g);
}
@Override
public boolean isAuthorized(HashMap<String, String> security, String serviceName, String method) {
/*
* check not needed if (security == null) { // internal messaging return
* defaultAccess; }
*/
// TODO - super cache Radix Tree ??? super key -- uri
// user:password@mrl://someService/someMethod - not found | ALLOWED ||
// DENIED
// user versus binary token
if (security.containsKey("user")) // && password || token
{
String fromUser = security.get("user");
// user scheme found - get the group
if (!users.containsKey(fromUser)) {
// invoke UserNotFound / throw
return false;
} else {
User user = users.get(fromUser);
// check MD5 hash of password
// FIXME OPTIMIZE - GENERATE KEY user.group.accessRule - ALLOW ?
// I'm looking for a specific object method - should have that
// key
if (!groups.containsKey(user.groupId)) // FIXME - optimize only
// need a group look up
// not a user l
{
// credentials supplied - no match
// invoke Group for this user not found
return false;
} else {
// credentials supplied - match - check access rules
Group group = groups.get(user.groupId);
// make message key
// service level
if (group.accessRules.containsKey(serviceName)) {
return group.accessRules.get(serviceName);
}
// method level
String methodLevel = String.format("%s.%s", serviceName, method);
if (group.accessRules.containsKey(methodLevel)) {
return group.accessRules.get(methodLevel);
}
return group.defaultAccess;
}
}
} else {
// invoke UnavailableSecurityScheme
return false;
}
}
@Override
public boolean isAuthorized(Message msg) {
return isAuthorized(msg.security, msg.name, msg.method);
}
public boolean setDefaultNewGroupId(String userId, String groupId) {
if (!users.containsKey(userId)) {
error("user %s does not exist can not change groupId", userId);
return false;
}
if (!groups.containsKey(groupId)) {
error("group %s does not exist can not change groupId", groupId);
return false;
}
users.get(userId).groupId = groupId;
return false;
}
public boolean setGroup(String userId, String groupId) {
if (!users.containsKey(userId)) {
error("user %s does not exist", userId);
return false;
}
if (!groups.containsKey(groupId)) {
error("group %s does not exist", groupId);
return false;
}
User u = users.get(userId);
u.groupId = groupId;
return true;
}
/**
* This static method returns all the details of the class without it having
* to be constructed. It has description, categories, dependencies, and peer
* definitions.
*
* @return ServiceType - returns all the data
*
*/
static public ServiceType getMetaData() {
ServiceType meta = new ServiceType(Security.class.getCanonicalName());
meta.addDescription("provides security");
meta.addCategory("framework", "security");
return meta;
}
}