/*******************************************************************************
* Copyright (c) 2008-2009, G. Weirich and Elexis
* 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
*
* Contributors:
* G. Weirich - initial implementation
*
*******************************************************************************/
package ch.rgw.crypt;
import java.io.ByteArrayOutputStream;
import java.io.InputStream;
import java.io.OutputStreamWriter;
import java.io.Serializable;
import java.net.URL;
import java.net.URLConnection;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.jdom.Document;
import org.jdom.Element;
import ch.rgw.crypt.Cryptologist.VERIFY_RESULT;
import ch.rgw.tools.ExHandler;
import ch.rgw.tools.Result;
import ch.rgw.tools.SoapConverter;
import ch.rgw.tools.StringTool;
/**
* Secure Authenticated Transmission The Transmitter stores a hashtable in an encrypted and signed
* wrapper.
*
* @author Gerry
*
*/
public class SAT {
/** A key to store the return value of a call */
public static final String KEY_RESULT = "return";
/** A key to store error conditions */
public static final String KEY_ERROR = "error";
public static final String USER_UNKNOWN = "User unknown";
public static final String RESULT_BAD_SIGNATURE = "Bad signature";
public static final String ADM_TIMESTAMP = "ADM_timestamp";
public static final String ADM_SIGNED_BY = "ADM_user";
public static final String ADM_PAYLOAD = "ADM_payload";
public static final String ADM_SIGNATURE = "ADM_signature";
public static final String ERR_SERVER = "Server error: ";
public static final String ERR_DECRYPT = "Decrypt error: ";
private static final String VERSION = "0.3.0";
private String ident = "xidClient";
private String prov = "elexis.ch";
Cryptologist crypt;
/**
* Create a new SAT actor
*
* @param c
* a fully configured Cryptologist
*/
public SAT(Cryptologist c){
crypt = c;
}
public SAT(String creator, String provider, Cryptologist c){
this(c);
ident = creator;
prov = provider;
}
/**
* Decrypt, and verify a packet (using our preconfigured Cryptologist)
*
* @param encrypted
* the encrypted packet
* @return a hashmap with the Parameters and an additional parameter "ADM_SIGNED_BY" containing
* the sender's ID
*/
public Map<String, Serializable> unwrap(byte[] encrypted, boolean bCheckSignature)
throws CryptologistException{
if (encrypted == null) {
throw new CryptologistException("Null packet from server",
CryptologistException.ERR_BAD_PARAMETER);
}
if (encrypted.length < 35) { // is probably an error message
throw new CryptologistException(new String(encrypted),
CryptologistException.ERR_SHORT_BLOCK);
}
Result<byte[]> dec = crypt.decrypt(encrypted);
if ((dec == null) || (!dec.isOK())) {
throw new CryptologistException("Decryption failed: " + dec == null ? "dec is null"
: String.valueOf(dec), CryptologistException.ERR_DECRYPTION_FAILURE);
}
byte[] decrypted = dec.get();
SoapConverter sc = new SoapConverter();
if (sc.load(decrypted)) {
Map<String, Serializable> fields = sc.getParameters();
String user = (String) fields.get(ADM_SIGNED_BY);
Long ts = (Long) fields.get(ADM_TIMESTAMP);
byte[] signature = (byte[]) fields.get(ADM_SIGNATURE);
if ((StringTool.isNothing(user)) || (signature == null)) {
throw new CryptologistException("Bad protocol",
CryptologistException.ERR_BAD_PROTOCOL);
}
if (ts == null || ((System.currentTimeMillis() - ts) > 300000)) {
throw new CryptologistException("timeout", CryptologistException.ERR_TIMEOUT);
}
Map<String, Serializable> ret = (Map<String, Serializable>) fields.get(ADM_PAYLOAD);
if (!bCheckSignature) {
return ret;
}
byte[] digest = calcDigest(sc);
if (crypt.verify(digest, signature, user) == VERIFY_RESULT.OK) {
ret.put(ADM_SIGNED_BY, user);
return ret;
} else {
throw new CryptologistException(USER_UNKNOWN + ": " + user,
CryptologistException.ERR_USER_UNKNOWN);
}
} else {
HashMap<String, Serializable> result = new HashMap<String, Serializable>();
result.put(KEY_ERROR, "Invalid Message");
return result;
}
}
/**
* Sign and encrypt a HashMap. We sign the unencrypted data and encrypt later. This imposes more
* load on verifying (since it must be decrypted prior to verify the signature) but improves
* stability against replay attacks.
*
* @param hash
* a hashtable containing arbitrary String/Object pairs. All objects must be
* Serializables. Keynames starting with ADM_ are reserved and must not be used.
* @param dest
* the receiver. The Object will be encoded with the receiver's public key
* @return a byte array containing the signed and encrypted Hashmap. This will remain valid for
* 5 Minutes.
* @throws Exception
*/
public byte[] wrap(Map<String, Serializable> var, String dest) throws CryptologistException{
SoapConverter sc = new SoapConverter();
sc.create(ident, VERSION, prov);
try {
sc.addMap(null, ADM_PAYLOAD, var);
} catch (Exception ex) {
throw new CryptologistException("Internal Cryptologist error: " + ex.getMessage(),
CryptologistException.ERR_INTERNAL);
}
sc.addIntegral(ADM_TIMESTAMP, System.currentTimeMillis());
sc.addString(ADM_SIGNED_BY, crypt.getUser());
byte[] digest = calcDigest(sc);
byte[] signature = crypt.sign(digest);
sc.addArray(ADM_SIGNATURE, signature);
String xml = sc.toString();
byte[] wrapped = crypt.encrypt(StringTool.getBytes(xml), dest);
if (wrapped == null) {
throw new CryptologistException("Encry<ption failed",
CryptologistException.ERR_ENCRYPTION_FAILURE);
}
return wrapped;
}
@Deprecated
public String sendRequest(String hostaddress, String request){
try {
// Connect to server
URLConnection conn = (new URL(hostaddress)).openConnection();
// Get output stream
conn.setDoOutput(true);
OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
String rx = new String(Base64Coder.encodeString(request));
out.write("request=" + rx);
out.close();
StringBuilder sb = new StringBuilder();
InputStream is = conn.getInputStream();
int in;
while (((in = is.read()) != -1)) {
sb.append((byte) in);
}
return Base64Coder.decodeString(sb.toString()); // .decodeBuffer(conn.getInputStream());
} catch (Exception ex) {
ExHandler.handle(ex);
return "";
}
}
public byte[] sendRequest(String hostaddress, byte[] request){
try {
// Connect to server
URLConnection conn = (new URL(hostaddress)).openConnection();
// Get output stream
conn.setDoOutput(true);
OutputStreamWriter out = new OutputStreamWriter(conn.getOutputStream());
// String rx = new String(Base64Coder.encode(request));
String rx = StringTool.enPrintableStrict(request);
out.write("request=" + rx);
out.close();
// StringBuilder sb = new StringBuilder();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
InputStream is = conn.getInputStream();
int in;
while (((in = is.read()) != -1)) {
// sb.append((byte) in);
baos.write(in);
}
// return Base64Coder.decode(sb.toString()); //
// .decodeBuffer(conn.getInputStream());
return baos.toByteArray();
} catch (Exception ex) {
ExHandler.handle(ex);
return null;
}
}
private byte[] calcDigest(SoapConverter sc){
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
Document doc = sc.getXML();
Element eRoot = doc.getRootElement();
Element body = eRoot.getChild("Body", SoapConverter.ns);
addParameters(body, digest);
return digest.digest();
} catch (NoSuchAlgorithmException e) {
ExHandler.handle(e);
}
return null;
}
private void addParameters(Element e, MessageDigest digest){
List<Element> params = e.getChildren("parameter", SoapConverter.ns);
for (Element el : params) {
String type = el.getAttributeValue("type");
String name = el.getAttributeValue("name");
if (type.equalsIgnoreCase(SoapConverter.TYPE_MAP)) {
addParameters(el, digest);
} else if (name.equalsIgnoreCase(ADM_SIGNATURE)) {
continue;
} else {
digest.update(StringTool.getBytes(type));
digest.update(StringTool.getBytes(name));
digest.update(StringTool.getBytes(el.getTextTrim()));
}
}
}
}